feat: preload textual model

This commit is contained in:
martabal
2024-09-25 17:39:55 +02:00
594 changed files with 10932 additions and 8871 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
This project uses the [SvelteKit](https://kit.svelte.dev/) web framework. Please refer to [the SvelteKit docs](https://kit.svelte.dev/docs) for information on getting started as a contributor to this project. In particular, it will help you navigate the project's code if you understand the basics of [SvelteKit routing](https://kit.svelte.dev/docs/routing).
When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [../server](the server project).
When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [the server project](../server).
+276 -539
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -67,7 +67,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.7.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1",
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
+1 -1
View File
@@ -74,7 +74,7 @@
if (!theme) {
theme = { value: 'light', system: true };
} else if (theme === 'dark' || theme === 'light') {
theme = { value: item, system: false };
theme = { value: theme, system: false };
localStorage.setItem(colorThemeKeyName, JSON.stringify(theme));
} else {
theme = JSON.parse(theme);
@@ -1,25 +1,21 @@
<script lang="ts">
import Checkbox from '$lib/components/elements/checkbox.svelte';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import { serverConfig } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
import { deleteUserAdmin, type UserResponseDto } from '@immich/sdk';
import { serverConfig } from '$lib/stores/server-config.store';
import { createEventDispatcher } from 'svelte';
import Checkbox from '$lib/components/elements/checkbox.svelte';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let user: UserResponseDto;
export let onSuccess: () => void;
export let onFail: () => void;
export let onCancel: () => void;
let forceDelete = false;
let deleteButtonDisabled = false;
let userIdInput: string = '';
const dispatch = createEventDispatcher<{
success: void;
fail: void;
cancel: void;
}>();
const handleDeleteUser = async () => {
try {
const { deletedAt } = await deleteUserAdmin({
@@ -28,13 +24,13 @@
});
if (deletedAt == undefined) {
dispatch('fail');
onFail();
} else {
dispatch('success');
onSuccess();
}
} catch (error) {
handleError(error, $t('errors.unable_to_delete_user'));
dispatch('fail');
onFail();
}
};
@@ -48,7 +44,7 @@
title={$t('delete_user')}
confirmText={forceDelete ? $t('permanently_delete') : $t('delete')}
onConfirm={handleDeleteUser}
onCancel={() => dispatch('cancel')}
{onCancel}
disabled={deleteButtonDisabled}
>
<svelte:fragment slot="prompt">
@@ -1,5 +1,6 @@
<script lang="ts">
import Badge from '$lib/components/elements/badge.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { locale } from '$lib/stores/preferences.store';
import { JobCommand, type JobCommandDto, type JobCountsDto, type QueueStatusDto } from '@immich/sdk';
@@ -12,11 +13,10 @@
mdiPlay,
mdiSelectionSearch,
} from '@mdi/js';
import { createEventDispatcher, type ComponentType } from 'svelte';
import { type ComponentType } from 'svelte';
import { t } from 'svelte-i18n';
import JobTileButton from './job-tile-button.svelte';
import JobTileStatus from './job-tile-status.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { t } from 'svelte-i18n';
export let title: string;
export let subtitle: string | undefined;
@@ -29,13 +29,12 @@
export let allText: string;
export let missingText: string;
export let onCommand: (command: JobCommandDto) => void;
$: waitingCount = jobCounts.waiting + jobCounts.paused + jobCounts.delayed;
$: isIdle = !queueStatus.isActive && !queueStatus.isPaused;
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6';
const dispatch = createEventDispatcher<{ command: JobCommandDto }>();
</script>
<div
@@ -66,7 +65,7 @@
title={$t('clear_message')}
size="12"
padding="1"
on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
on:click={() => onCommand({ command: JobCommand.ClearFailed, force: false })}
/>
</div>
</Badge>
@@ -117,54 +116,42 @@
<JobTileButton
disabled={true}
color="light-gray"
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
on:click={() => onCommand({ command: JobCommand.Start, force: false })}
>
<Icon path={mdiAlertCircle} size="36" />
{$t('disabled').toUpperCase()}
</JobTileButton>
{:else if !isIdle}
{#if waitingCount > 0}
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
<JobTileButton color="gray" on:click={() => onCommand({ command: JobCommand.Empty, force: false })}>
<Icon path={mdiClose} size="24" />
{$t('clear').toUpperCase()}
</JobTileButton>
{/if}
{#if queueStatus.isPaused}
{@const size = waitingCount > 0 ? '24' : '48'}
<JobTileButton
color="light-gray"
on:click={() => dispatch('command', { command: JobCommand.Resume, force: false })}
>
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Resume, force: false })}>
<!-- size property is not reactive, so have to use width and height -->
<Icon path={mdiFastForward} {size} />
{$t('resume').toUpperCase()}
</JobTileButton>
{:else}
<JobTileButton
color="light-gray"
on:click={() => dispatch('command', { command: JobCommand.Pause, force: false })}
>
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Pause, force: false })}>
<Icon path={mdiPause} size="24" />
{$t('pause').toUpperCase()}
</JobTileButton>
{/if}
{:else if allowForceCommand}
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Start, force: true })}>
<JobTileButton color="gray" on:click={() => onCommand({ command: JobCommand.Start, force: true })}>
<Icon path={mdiAllInclusive} size="24" />
{allText}
</JobTileButton>
<JobTileButton
color="light-gray"
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
>
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Start, force: false })}>
<Icon path={mdiSelectionSearch} size="24" />
{missingText}
</JobTileButton>
{:else}
<JobTileButton
color="light-gray"
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
>
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Start, force: false })}>
<Icon path={mdiPlay} size="48" />
{$t('start').toUpperCase()}
</JobTileButton>
@@ -163,7 +163,7 @@
{allowForceCommand}
{jobCounts}
{queueStatus}
on:command={({ detail }) => (handleCommandOverride || handleCommand)(jobName, detail)}
onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)}
/>
{/each}
</div>
@@ -3,28 +3,24 @@
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import { handleError } from '$lib/utils/handle-error';
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
export let user: UserResponseDto;
const dispatch = createEventDispatcher<{
success: void;
fail: void;
cancel: void;
}>();
export let onSuccess: () => void;
export let onFail: () => void;
export let onCancel: () => void;
const handleRestoreUser = async () => {
try {
const { deletedAt } = await restoreUserAdmin({ id: user.id });
if (deletedAt == undefined) {
dispatch('success');
onSuccess();
} else {
dispatch('fail');
onFail();
}
} catch (error) {
handleError(error, $t('errors.unable_to_restore_user'));
dispatch('fail');
onFail();
}
};
</script>
@@ -34,7 +30,7 @@
confirmText={$t('continue')}
confirmColor="green"
onConfirm={handleRestoreUser}
onCancel={() => dispatch('cancel')}
{onCancel}
>
<svelte:fragment slot="prompt">
<p>
@@ -71,7 +71,7 @@
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4">
<div class="ml-4 mt-4 flex flex-col">
<SettingAccordion
key="oauth"
title={$t('admin.oauth_settings')}
@@ -99,7 +99,7 @@
]}
name="vcodec"
isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec}
on:select={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])}
onSelect={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])}
/>
<SettingSelect
@@ -114,7 +114,7 @@
]}
name="acodec"
isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec}
on:select={() =>
onSelect={() =>
config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)
? null
: config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)}
@@ -96,7 +96,7 @@
title={$t('admin.image_prefer_wide_gamut')}
subtitle={$t('admin.image_prefer_wide_gamut_setting_description')}
checked={config.image.colorspace === Colorspace.P3}
on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
onToggle={(isChecked) => (config.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)}
isEdited={config.image.colorspace !== savedConfig.image.colorspace}
{disabled}
/>
@@ -105,7 +105,7 @@
title={$t('admin.image_prefer_embedded_preview')}
subtitle={$t('admin.image_prefer_embedded_preview_setting_description')}
checked={config.image.extractEmbedded}
on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
onToggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded}
{disabled}
/>
@@ -177,7 +177,7 @@
desc={$t('admin.machine_learning_min_detection_score_description')}
bind:value={config.machineLearning.facialRecognition.minScore}
step="0.1"
min={0}
min={0.1}
max={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
isEdited={config.machineLearning.facialRecognition.minScore !==
@@ -190,7 +190,7 @@
desc={$t('admin.machine_learning_max_recognition_distance_description')}
bind:value={config.machineLearning.facialRecognition.maxDistance}
step="0.1"
min={0}
min={0.1}
max={2}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
isEdited={config.machineLearning.facialRecognition.maxDistance !==
@@ -2,7 +2,6 @@
import Icon from '$lib/components/elements/icon.svelte';
import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk';
import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
@@ -16,6 +15,9 @@
export let order: AssetOrder | undefined;
export let user: UserResponseDto;
export let onChangeOrder: (order: AssetOrder) => void;
export let onClose: () => void;
export let onToggleEnabledActivity: () => void;
export let onShowSelectSharedUser: () => void;
const options: Record<AssetOrder, RenderedOption> = {
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
@@ -24,12 +26,6 @@
$: selectedOption = order ? options[order] : options[AssetOrder.Desc];
const dispatch = createEventDispatcher<{
close: void;
toggleEnableActivity: void;
showSelectSharedUser: void;
}>();
const handleToggle = async (returnedOption: RenderedOption) => {
if (selectedOption === returnedOption) {
return;
@@ -51,7 +47,7 @@
};
</script>
<FullScreenModal title={$t('options')} onClose={() => dispatch('close')}>
<FullScreenModal title={$t('options')} {onClose}>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
@@ -68,14 +64,14 @@
title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled}
on:toggle={() => dispatch('toggleEnableActivity')}
onToggle={onToggleEnabledActivity}
/>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2">
<button type="button" class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
<button type="button" class="flex items-center gap-2" on:click={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
@@ -7,6 +7,7 @@
export let id: string;
export let albumName: string;
export let isOwned: boolean;
export let onUpdate: (albumName: string) => void;
$: newAlbumName = albumName;
@@ -16,17 +17,17 @@
}
try {
await updateAlbumInfo({
({ albumName } = await updateAlbumInfo({
id,
updateAlbumDto: {
albumName: newAlbumName,
},
});
}));
onUpdate(albumName);
} catch (error) {
handleError(error, $t('errors.unable_to_save_album'));
return;
}
albumName = newAlbumName;
};
</script>
@@ -154,7 +154,7 @@
title={$t('sort_albums_by')}
options={Object.values(sortOptionsMetadata)}
selectedOption={selectedSortOption}
on:select={({ detail }) => handleChangeSortBy(detail)}
onSelect={handleChangeSortBy}
render={({ id }) => ({
title: albumSortByNames[id],
icon: sortIcon,
@@ -166,7 +166,7 @@
title={$t('group_albums_by')}
options={Object.values(groupOptionsMetadata)}
selectedOption={selectedGroupOption}
on:select={({ detail }) => handleChangeGroupBy(detail)}
onSelect={handleChangeGroupBy}
render={({ id, isDisabled }) => ({
title: albumGroupByNames[id],
icon: groupIcon,
@@ -394,13 +394,13 @@
<CreateSharedLinkModal
albumId={albumToShare.id}
onClose={() => closeShareModal()}
on:created={() => albumToShare && handleSharedLinkCreated(albumToShare)}
onCreated={() => albumToShare && handleSharedLinkCreated(albumToShare)}
/>
{:else}
<UserSelectionModal
album={albumToShare}
on:select={({ detail: users }) => handleAddUsers(users)}
on:share={() => (showShareByURLModal = true)}
onSelect={handleAddUsers}
onShare={() => (showShareByURLModal = true)}
onClose={() => closeShareModal()}
/>
{/if}
@@ -8,7 +8,7 @@
AlbumUserRole,
} from '@immich/sdk';
import { mdiDotsVertical } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { handleError } from '../../utils/handle-error';
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
@@ -20,11 +20,8 @@
export let album: AlbumResponseDto;
export let onClose: () => void;
const dispatch = createEventDispatcher<{
remove: string;
refreshAlbum: void;
}>();
export let onRemove: (userId: string) => void;
export let onRefreshAlbum: () => void;
let currentUser: UserResponseDto;
let selectedRemoveUser: UserResponseDto | null = null;
@@ -52,7 +49,7 @@
try {
await removeUserFromAlbum({ id: album.id, userId });
dispatch('remove', userId);
onRemove(userId);
const message =
userId === 'me'
? $t('album_user_left', { values: { album: album.albumName } })
@@ -71,7 +68,7 @@
const message = $t('user_role_set', {
values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') },
});
dispatch('refreshAlbum');
onRefreshAlbum();
notificationController.show({ type: NotificationType.Info, message });
} catch (error) {
handleError(error, $t('errors.unable_to_change_album_user_role'));
@@ -13,13 +13,16 @@
type UserResponseDto,
} from '@immich/sdk';
import { mdiCheck, mdiEye, mdiLink, mdiPencil, mdiShareCircle } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto;
export let onClose: () => void;
export let onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
export let onShare: () => void;
let users: UserResponseDto[] = [];
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
@@ -29,10 +32,6 @@
{ title: $t('remove_user'), value: 'none' },
];
const dispatch = createEventDispatcher<{
select: AlbumUserAddDto[];
share: void;
}>();
let sharedLinks: SharedLinkResponseDto[] = [];
onMount(async () => {
await getSharedLinks();
@@ -99,7 +98,7 @@
title={$t('role')}
options={roleOptions}
render={({ title, icon }) => ({ title, icon })}
on:select={({ detail: { value } }) => handleChangeRole(user, value)}
onSelect={({ value }) => handleChangeRole(user, value)}
/>
</div>
{/key}
@@ -152,10 +151,8 @@
rounded="full"
disabled={Object.keys(selectedUsers).length === 0}
on:click={() =>
dispatch(
'select',
Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
)}>{$t('add')}</Button
onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))}
>{$t('add')}</Button
>
</div>
{/if}
@@ -166,7 +163,7 @@
<button
type="button"
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
on:click={() => dispatch('share')}
on:click={onShare}
>
<Icon path={mdiLink} size={24} />
<p class="text-sm">{$t('create_link')}</p>
@@ -40,8 +40,8 @@
<Portal target="body">
<AlbumSelectionModal
{shared}
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
on:album={({ detail }) => handleAddToAlbum(detail)}
onNewAlbum={handleAddToNewAlbum}
onAlbumClick={handleAddToAlbum}
onClose={() => (showSelectionModal = false)}
/>
</Portal>
@@ -82,6 +82,6 @@
{#if showConfirmModal}
<Portal target="body">
<DeleteAssetDialog size={1} on:cancel={() => (showConfirmModal = false)} on:confirm={() => deleteAsset()} />
<DeleteAssetDialog size={1} onCancel={() => (showConfirmModal = false)} onConfirm={deleteAsset} />
</Portal>
{/if}
@@ -2,26 +2,22 @@
import { locale } from '$lib/stores/preferences.store';
import type { ActivityResponseDto } from '@immich/sdk';
import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import Icon from '../elements/icon.svelte';
export let isLiked: ActivityResponseDto | null;
export let numberOfComments: number | undefined;
export let disabled: boolean;
const dispatch = createEventDispatcher<{
openActivityTab: void;
favorite: void;
}>();
export let onOpenActivityTab: () => void;
export let onFavorite: () => void;
</script>
<div class="w-full flex p-4 text-white items-center justify-center rounded-full gap-5 bg-immich-dark-bg bg-opacity-60">
<button type="button" class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}>
<button type="button" class={disabled ? 'cursor-not-allowed' : ''} on:click={onFavorite} {disabled}>
<div class="items-center justify-center">
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
</div>
</button>
<button type="button" on:click={() => dispatch('openActivityTab')}>
<button type="button" on:click={onOpenActivityTab}>
<div class="flex gap-2 items-center justify-center">
<Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
{#if numberOfComments}
@@ -17,7 +17,7 @@
} from '@immich/sdk';
import { mdiClose, mdiDotsVertical, mdiHeart, mdiSend, mdiDeleteOutline } from '@mdi/js';
import * as luxon from 'luxon';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
@@ -55,6 +55,10 @@
export let albumOwnerId: string;
export let disabled: boolean;
export let isLiked: ActivityResponseDto | null;
export let onDeleteComment: () => void;
export let onDeleteLike: () => void;
export let onAddComment: () => void;
export let onClose: () => void;
let textArea: HTMLTextAreaElement;
let innerHeight: number;
@@ -65,13 +69,6 @@
let message = '';
let isSendingMessage = false;
const dispatch = createEventDispatcher<{
deleteComment: void;
deleteLike: void;
addComment: void;
close: void;
}>();
$: {
if (innerHeight && activityHeight) {
divHeight = innerHeight - activityHeight;
@@ -111,9 +108,9 @@
reactions.splice(index, 1);
reactions = reactions;
if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) {
dispatch('deleteLike');
onDeleteLike();
} else {
dispatch('deleteComment');
onDeleteComment();
}
const deleteMessages: Record<ReactionType, string> = {
@@ -141,7 +138,7 @@
reactions.push(data);
textArea.style.height = '18px';
message = '';
dispatch('addComment');
onAddComment();
// Re-render the activity feed
reactions = reactions;
} catch (error) {
@@ -160,7 +157,7 @@
bind:clientHeight={activityHeight}
>
<div class="flex place-items-center gap-2">
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} title={$t('close')} />
<CircleIconButton on:click={onClose} icon={mdiClose} title={$t('close')} />
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('activity')}</p>
</div>
@@ -1,16 +1,13 @@
<script lang="ts">
import { getAssetThumbnailUrl } from '$lib/utils';
import { type AlbumResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { normalizeSearchString } from '$lib/utils/string-utils.js';
import AlbumListItemDetails from './album-list-item-details.svelte';
const dispatch = createEventDispatcher<{
album: void;
}>();
export let album: AlbumResponseDto;
export let searchQuery = '';
export let onAlbumClick: () => void;
let albumNameArray: string[] = ['', '', ''];
// This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query
@@ -29,7 +26,7 @@
<button
type="button"
on:click={() => dispatch('album')}
on:click={onAlbumClick}
class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
>
<span class="h-12 w-12 shrink-0 rounded-xl bg-slate-300">
@@ -32,7 +32,7 @@
type AssetResponseDto,
type StackResponseDto,
} from '@immich/sdk';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
@@ -56,8 +56,10 @@
export let isShared = false;
export let album: AlbumResponseDto | null = null;
export let onAction: OnAction | undefined = undefined;
let reactions: ActivityResponseDto[] = [];
export let reactions: ActivityResponseDto[] = [];
export let onClose: (dto: { asset: AssetResponseDto }) => void;
export let onNext: () => void;
export let onPrevious: () => void;
const { setAsset } = assetViewingStore;
const {
@@ -67,13 +69,6 @@
slideshowState,
} = slideshowStore;
const dispatch = createEventDispatcher<{
action: { type: AssetAction; asset: AssetResponseDto };
close: { asset: AssetResponseDto };
next: void;
previous: void;
}>();
let appearsInAlbums: AlbumResponseDto[] = [];
let shouldPlayMotionPhoto = false;
let sharedLink = getSharedLink();
@@ -267,7 +262,7 @@
};
const closeViewer = () => {
dispatch('close', { asset });
onClose({ asset });
};
const closeEditor = () => {
@@ -316,7 +311,8 @@
}
e?.stopPropagation();
dispatch(order);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
order === 'previous' ? onPrevious() : onNext();
};
// const showEditorHandler = () => {
@@ -533,8 +529,8 @@
disabled={!album?.isActivityEnabled}
{isLiked}
{numberOfComments}
on:favorite={handleFavorite}
on:openActivityTab={handleOpenActivity}
onFavorite={handleFavorite}
onOpenActivityTab={handleOpenActivity}
/>
</div>
{/if}
@@ -555,7 +551,7 @@
class="z-[1002] row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
translate="yes"
>
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} on:close={() => ($isShowDetail = false)} />
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
</div>
{/if}
@@ -625,10 +621,10 @@
assetId={asset.id}
{isLiked}
bind:reactions
on:addComment={handleAddComment}
on:deleteComment={handleRemoveComment}
on:deleteLike={() => (isLiked = null)}
on:close={() => (isShowActivity = false)}
onAddComment={handleAddComment}
onDeleteComment={handleRemoveComment}
onDeleteLike={() => (isLiked = null)}
onClose={() => (isShowActivity = false)}
/>
</div>
{/if}
@@ -81,10 +81,6 @@
{#if isShowChangeLocation}
<Portal>
<ChangeLocation
{asset}
on:confirm={({ detail: gps }) => handleConfirmChangeLocation(gps)}
on:cancel={() => (isShowChangeLocation = false)}
/>
<ChangeLocation {asset} onConfirm={handleConfirmChangeLocation} onCancel={() => (isShowChangeLocation = false)} />
</Portal>
{/if}
@@ -1,5 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation';
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants';
@@ -9,6 +13,9 @@
import { preferences, user } from '$lib/stores/user.store';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils';
import { delay, isFlipped } from '$lib/utils/asset-utils';
import { getByteUnitString } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { fromDateTimeOriginal, fromLocalDateTime } from '$lib/utils/timeline-util';
import {
AssetMediaSize,
getAssetInfo,
@@ -18,6 +25,7 @@
type ExifResponseDto,
} from '@immich/sdk';
import {
mdiAccountOff,
mdiCalendar,
mdiCameraIris,
mdiClose,
@@ -26,28 +34,21 @@
mdiImageOutline,
mdiInformationOutline,
mdiPencil,
mdiAccountOff,
} from '@mdi/js';
import { DateTime } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { slide } from 'svelte/transition';
import { getByteUnitString } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import PersonSidePanel from '../faces-page/person-side-panel.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import AlbumListItemDetails from './album-list-item-details.svelte';
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
import { t } from 'svelte-i18n';
import { goto } from '$app/navigation';
import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = [];
export let currentAlbum: AlbumResponseDto | null = null;
export let onClose: () => void;
const getDimensions = (exifInfo: ExifResponseDto) => {
const { exifImageWidth: width, exifImageHeight: height } = exifInfo;
@@ -99,9 +100,11 @@
$: unassignedFaces = asset.unassignedFaces || [];
const dispatch = createEventDispatcher<{
close: void;
}>();
$: timeZone = asset.exifInfo?.timeZone;
$: dateTime =
timeZone && asset.exifInfo?.dateTimeOriginal
? fromDateTimeOriginal(asset.exifInfo.dateTimeOriginal, timeZone)
: fromLocalDateTime(asset.localDateTime);
const getMegapixel = (width: number, height: number): number | undefined => {
const megapixel = Math.round((height * width) / 1_000_000);
@@ -137,7 +140,7 @@
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<div class="flex place-items-center gap-2">
<CircleIconButton icon={mdiClose} title={$t('close')} on:click={() => dispatch('close')} />
<CircleIconButton icon={mdiClose} title={$t('close')} on:click={onClose} />
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p>
</div>
@@ -261,10 +264,7 @@
<p class="text-sm">{$t('no_exif_info_available').toUpperCase()}</p>
{/if}
{#if asset.exifInfo?.dateTimeOriginal}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
{#if dateTime}
<button
type="button"
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
@@ -280,7 +280,7 @@
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{dateTime.toLocaleString(
{
month: 'short',
day: 'numeric',
@@ -291,12 +291,12 @@
</p>
<div class="flex gap-2 text-sm">
<p>
{assetDateTimeOriginal.toLocaleString(
{dateTime.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
timeZoneName: timeZone ? 'longOffset' : undefined,
},
{ locale: $locale },
)}
@@ -311,7 +311,7 @@
</div>
{/if}
</button>
{:else if !asset.exifInfo?.dateTimeOriginal && isOwner}
{:else if !dateTime && isOwner}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4">
<div>
@@ -325,18 +325,11 @@
{/if}
{#if isShowChangeDate}
{@const assetDateTimeOriginal = asset.exifInfo?.dateTimeOriginal
? DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
locale: $locale,
})
: DateTime.now()}
{@const assetTimeZoneOriginal = asset.exifInfo?.timeZone ?? ''}
<ChangeDate
initialDate={assetDateTimeOriginal}
initialTimeZone={assetTimeZoneOriginal}
on:confirm={({ detail: date }) => handleConfirmChangeDate(date)}
on:cancel={() => (isShowChangeDate = false)}
initialDate={dateTime}
initialTimeZone={timeZone ?? ''}
onConfirm={handleConfirmChangeDate}
onCancel={() => (isShowChangeDate = false)}
/>
{/if}
@@ -514,9 +507,7 @@
<PersonSidePanel
assetId={asset.id}
assetType={asset.type}
on:close={() => {
showEditFaces = false;
}}
on:refresh={handleRefreshPeople}
onClose={() => (showEditFaces = false)}
onRefresh={handleRefreshPeople}
/>
{/if}
@@ -139,5 +139,5 @@
duration={$slideshowDelay}
bind:this={progressBar}
bind:status={progressBarStatus}
on:done={handleDone}
onDone={handleDone}
/>
@@ -4,7 +4,7 @@
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { AssetMediaSize } from '@immich/sdk';
import { createEventDispatcher, tick } from 'svelte';
import { tick } from 'svelte';
import { swipe } from 'svelte-gestures';
import type { SwipeCustomEvent } from 'svelte-gestures';
import { fade } from 'svelte/transition';
@@ -13,8 +13,10 @@
export let assetId: string;
export let loopVideo: boolean;
export let checksum: string;
export let onPreviousAsset: () => void;
export let onNextAsset: () => void;
export let onPreviousAsset: () => void = () => {};
export let onNextAsset: () => void = () => {};
export let onVideoEnded: () => void = () => {};
export let onVideoStarted: () => void = () => {};
let element: HTMLVideoElement | undefined = undefined;
let isVideoLoading = true;
@@ -27,12 +29,10 @@
element.load();
}
const dispatch = createEventDispatcher<{ onVideoEnded: void; onVideoStarted: void }>();
const handleCanPlay = async (video: HTMLVideoElement) => {
try {
await video.play();
dispatch('onVideoStarted');
onVideoStarted();
} catch (error) {
if (error instanceof DOMException && error.name === 'NotAllowedError' && !forceMuted) {
await tryForceMutedPlay(video);
@@ -75,7 +75,7 @@
use:swipe
on:swipe={onSwipe}
on:canplay={(e) => handleCanPlay(e.currentTarget)}
on:ended={() => dispatch('onVideoEnded')}
on:ended={onVideoEnded}
on:volumechange={(e) => {
if (!forceMuted) {
$videoViewerMuted = e.currentTarget.muted;
@@ -19,22 +19,18 @@
import LinkButton from './buttons/link-button.svelte';
import { clickOutside } from '$lib/actions/click-outside';
import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
let className = '';
export { className as class };
const dispatch = createEventDispatcher<{
select: T;
'click-outside': void;
}>();
export let options: T[];
export let selectedOption = options[0];
export let showMenu = false;
export let controlable = false;
export let hideTextOnSmallScreen = true;
export let title: string | undefined = undefined;
export let onSelect: (option: T) => void;
export let onClickOutside: () => void = () => {};
export let render: (item: T) => string | RenderedOption = String;
@@ -43,11 +39,11 @@
showMenu = false;
}
dispatch('click-outside');
onClickOutside();
};
const handleSelectOption = (option: T) => {
dispatch('select', option);
onSelect(option);
selectedOption = option;
showMenu = false;
@@ -1,6 +1,5 @@
<script lang="ts">
import { mdiClose, mdiMagnify } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import type { SearchOptions } from '$lib/utils/dipatch';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
@@ -10,20 +9,20 @@
export let roundedBottom = true;
export let showLoadingSpinner: boolean;
export let placeholder: string;
export let onSearch: (options: SearchOptions) => void = () => {};
export let onReset: () => void = () => {};
let inputRef: HTMLElement;
const dispatch = createEventDispatcher<{ search: SearchOptions; reset: void }>();
const resetSearch = () => {
name = '';
dispatch('reset');
onReset();
inputRef?.focus();
};
const handleSearch = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
dispatch('search', { force: true });
onSearch({ force: true });
}
};
</script>
@@ -38,7 +37,7 @@
title={$t('search')}
size="16"
padding="2"
on:click={() => dispatch('search', { force: true })}
on:click={() => onSearch({ force: true })}
/>
<input
class="w-full gap-2 bg-gray-200 dark:bg-immich-dark-gray dark:text-white"
@@ -47,7 +46,7 @@
bind:value={name}
bind:this={inputRef}
on:keydown={handleSearch}
on:input={() => dispatch('search', { force: false })}
on:input={() => onSearch({ force: false })}
/>
{#if showLoadingSpinner}
<div class="flex place-items-center">
@@ -1,6 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
/**
* Unique identifier for the checkbox element, used to associate labels with the input element.
*/
@@ -11,9 +9,9 @@
export let ariaDescribedBy: string | undefined = undefined;
export let checked = false;
export let disabled = false;
export let onToggle: ((checked: boolean) => void) | undefined = undefined;
const dispatch = createEventDispatcher<{ toggle: boolean }>();
const onToggle = (event: Event) => dispatch('toggle', (event.target as HTMLInputElement).checked);
const handleToggle = (event: Event) => onToggle?.((event.target as HTMLInputElement).checked);
</script>
<label class="relative inline-block h-[10px] w-[36px] flex-none">
@@ -22,7 +20,7 @@
class="disabled::cursor-not-allowed h-0 w-0 opacity-0 peer"
type="checkbox"
bind:checked
on:click={onToggle}
on:click={handleToggle}
{disabled}
aria-describedby={ariaDescribedBy}
/>
@@ -4,7 +4,6 @@
import { getPeopleThumbnailUrl } from '$lib/utils';
import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition';
import { photoViewer } from '$lib/stores/assets.store';
@@ -19,6 +18,9 @@
export let editedFace: AssetFaceResponseDto;
export let assetId: string;
export let assetType: AssetTypeEnum;
export let onClose: () => void;
export let onCreatePerson: (featurePhoto: string | null) => void;
export let onReassign: (person: PersonResponseDto) => void;
// loading spinners
let isShowLoadingNewPerson = false;
@@ -31,25 +33,16 @@
$: showPeople = searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden);
const dispatch = createEventDispatcher<{
close: void;
createPerson: string | null;
reassign: PersonResponseDto;
}>();
const handleBackButton = () => {
dispatch('close');
};
const handleCreatePerson = async () => {
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
const newFeaturePhoto = await zoomImageToBase64(editedFace, assetId, assetType, $photoViewer);
dispatch('createPerson', newFeaturePhoto);
onCreatePerson(newFeaturePhoto);
clearTimeout(timeout);
isShowLoadingNewPerson = false;
dispatch('createPerson', newFeaturePhoto);
onCreatePerson(newFeaturePhoto);
};
</script>
@@ -60,7 +53,7 @@
<div class="flex place-items-center justify-between gap-2">
{#if !searchFaces}
<div class="flex items-center gap-2">
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('select_face')}</p>
</div>
<div class="flex justify-end gap-2">
@@ -80,7 +73,7 @@
{/if}
</div>
{:else}
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
<div class="w-full flex">
<SearchPeople
type="input"
@@ -103,7 +96,7 @@
{#each showPeople as person (person.id)}
{#if !editedFace.person || person.id !== editedFace.person.id}
<div class="w-fit">
<button type="button" class="w-[90px]" on:click={() => dispatch('reassign', person)}>
<button type="button" class="w-[90px]" on:click={() => onReassign(person)}>
<div class="relative">
<ImageThumbnail
curve
@@ -1,6 +1,5 @@
<script lang="ts">
import { type PersonResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Button from '../elements/buttons/button.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
@@ -11,10 +10,7 @@
export let suggestedPeople: PersonResponseDto[];
export let thumbnailData: string;
export let isSearchingPeople: boolean;
const dispatch = createEventDispatcher<{
change: string;
}>();
export let onChange: (name: string) => void;
</script>
<div
@@ -26,7 +22,7 @@
<form
class="ml-4 flex w-full justify-between gap-16"
autocomplete="off"
on:submit|preventDefault={() => dispatch('change', name)}
on:submit|preventDefault={() => onChange(name)}
>
<SearchPeople
bind:searchName={name}
@@ -1,7 +1,6 @@
<script lang="ts">
import { getPeopleThumbnailUrl } from '$lib/utils';
import { type PersonResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
export let person: PersonResponseDto;
@@ -10,20 +9,13 @@
export let thumbnailSize: number | null = null;
export let circle = false;
export let border = false;
let dispatch = createEventDispatcher<{
click: PersonResponseDto;
}>();
const handleOnClicked = () => {
dispatch('click', person);
};
export let onClick: (person: PersonResponseDto) => void = () => {};
</script>
<button
type="button"
class="relative rounded-lg transition-all"
on:click={handleOnClicked}
on:click={() => onClick(person)}
disabled={!selectable}
style:width={thumbnailSize ? thumbnailSize + 'px' : '100%'}
style:height={thumbnailSize ? thumbnailSize + 'px' : '100%'}
@@ -6,7 +6,7 @@
import { handleError } from '$lib/utils/handle-error';
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
@@ -20,15 +20,13 @@
import { t } from 'svelte-i18n';
export let person: PersonResponseDto;
export let onBack: () => void;
export let onMerge: (mergedPerson: PersonResponseDto) => void;
let people: PersonResponseDto[] = [];
let selectedPeople: PersonResponseDto[] = [];
let screenHeight: number;
let dispatch = createEventDispatcher<{
back: void;
merge: PersonResponseDto;
}>();
$: hasSelection = selectedPeople.length > 0;
$: peopleToNotShow = [...selectedPeople, person];
@@ -37,10 +35,6 @@
people = data.people;
});
const onClose = () => {
dispatch('back');
};
const handleSwapPeople = async () => {
[person, selectedPeople[0]] = [selectedPeople[0], person];
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
@@ -88,7 +82,7 @@
message: $t('merged_people_count', { values: { count } }),
type: NotificationType.Info,
});
dispatch('merge', mergedPerson);
onMerge(mergedPerson);
} catch (error) {
handleError(error, $t('cannot_merge_people'));
}
@@ -101,7 +95,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
>
<ControlAppBar on:close={onClose}>
<ControlAppBar onClose={onBack}>
<svelte:fragment slot="leading">
{#if hasSelection}
{$t('selected_count', { values: { count: selectedPeople.length } })}
@@ -125,7 +119,7 @@
<div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
{#each selectedPeople as person (person.id)}
<div animate:flip={{ duration: 250, easing: quintOut }}>
<FaceThumbnail border circle {person} selectable thumbnailSize={120} on:click={() => onSelect(person)} />
<FaceThumbnail border circle {person} selectable thumbnailSize={120} onClick={() => onSelect(person)} />
</div>
{/each}
@@ -152,7 +146,7 @@
</div>
</div>
<PeopleList {people} {peopleToNotShow} {screenHeight} on:select={({ detail }) => onSelect(detail)} />
<PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} />
</section>
</section>
</section>
@@ -4,7 +4,6 @@
import { getPeopleThumbnailUrl } from '$lib/utils';
import { type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeft, mdiMerge } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Button from '../elements/buttons/button.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
@@ -13,25 +12,22 @@
export let personMerge1: PersonResponseDto;
export let personMerge2: PersonResponseDto;
export let potentialMergePeople: PersonResponseDto[];
export let onReject: () => void;
export let onConfirm: ([personMerge1, personMerge2]: [PersonResponseDto, PersonResponseDto]) => void;
export let onClose: () => void;
let choosePersonToMerge = false;
const title = personMerge2.name;
const dispatch = createEventDispatcher<{
reject: void;
confirm: [PersonResponseDto, PersonResponseDto];
close: void;
}>();
const changePersonToMerge = (newperson: PersonResponseDto) => {
const index = potentialMergePeople.indexOf(newperson);
const changePersonToMerge = (newPerson: PersonResponseDto) => {
const index = potentialMergePeople.indexOf(newPerson);
[potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]];
choosePersonToMerge = false;
};
</script>
<FullScreenModal title="{$t('merge_people')} - {title}" onClose={() => dispatch('close')}>
<FullScreenModal title="{$t('merge_people')} - {title}" {onClose}>
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
{#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
@@ -105,7 +101,7 @@
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
</div>
<svelte:fragment slot="sticky-bottom">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button>
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>{$t('yes')}</Button>
<Button fullwidth color="gray" on:click={onReject}>{$t('no')}</Button>
<Button fullwidth on:click={() => onConfirm([personMerge1, personMerge2])}>{$t('yes')}</Button>
</svelte:fragment>
</FullScreenModal>
@@ -9,7 +9,6 @@
mdiDotsVertical,
mdiEyeOffOutline,
} from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import { t } from 'svelte-i18n';
@@ -18,19 +17,12 @@
export let person: PersonResponseDto;
export let preload = false;
type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-people' | 'hide-person';
let dispatch = createEventDispatcher<{
'change-name': void;
'set-birth-date': void;
'merge-people': void;
'hide-person': void;
}>();
export let onChangeName: () => void;
export let onSetBirthDate: () => void;
export let onMergePeople: () => void;
export let onHidePerson: () => void;
let showVerticalDots = false;
const onMenuClick = (event: MenuItemEvent) => {
dispatch(event);
};
</script>
<div
@@ -76,18 +68,10 @@
icon={mdiDotsVertical}
title={$t('show_person_options')}
>
<MenuOption onClick={() => onMenuClick('hide-person')} icon={mdiEyeOffOutline} text={$t('hide_person')} />
<MenuOption onClick={() => onMenuClick('change-name')} icon={mdiAccountEditOutline} text={$t('change_name')} />
<MenuOption
onClick={() => onMenuClick('set-birth-date')}
icon={mdiCalendarEditOutline}
text={$t('set_date_of_birth')}
/>
<MenuOption
onClick={() => onMenuClick('merge-people')}
icon={mdiAccountMultipleCheckOutline}
text={$t('merge_people')}
/>
<MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
<MenuOption onClick={onSetBirthDate} icon={mdiCalendarEditOutline} text={$t('set_date_of_birth')} />
<MenuOption onClick={onMergePeople} icon={mdiAccountMultipleCheckOutline} text={$t('merge_people')} />
</ButtonContextMenu>
</div>
{/if}
@@ -1,6 +1,5 @@
<script lang="ts">
import { type PersonResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import FaceThumbnail from './face-thumbnail.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import { t } from 'svelte-i18n';
@@ -8,15 +7,13 @@
export let screenHeight: number;
export let people: PersonResponseDto[];
export let peopleToNotShow: PersonResponseDto[];
export let onSelect: (person: PersonResponseDto) => void;
let searchedPeopleLocal: PersonResponseDto[] = [];
let name = '';
let showPeople: PersonResponseDto[];
let dispatch = createEventDispatcher<{
select: PersonResponseDto;
}>();
$: {
showPeople = name ? searchedPeopleLocal : people;
showPeople = showPeople.filter(
@@ -35,15 +32,7 @@
>
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
{#each showPeople as person (person.id)}
<FaceThumbnail
{person}
on:click={() => {
dispatch('select', person);
}}
circle
border
selectable
/>
<FaceThumbnail {person} onClick={() => onSelect(person)} circle border selectable />
{/each}
</div>
</div>
@@ -83,8 +83,8 @@
bind:name={searchName}
{showLoadingSpinner}
{placeholder}
on:reset={handleReset}
on:search={({ detail }) => handleSearch(detail.force ?? false)}
onReset={handleReset}
onSearch={({ force }) => handleSearch(force ?? false)}
/>
{:else}
<input
@@ -18,7 +18,7 @@
import { mdiAccountOff } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiArrowLeftThin, mdiMinus, mdiRestart } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
@@ -31,6 +31,8 @@
export let assetId: string;
export let assetType: AssetTypeEnum;
export let onClose: () => void;
export let onRefresh: () => void;
// keep track of the changes
let peopleToCreate: string[] = [];
@@ -56,11 +58,6 @@
const thumbnailWidth = '90px';
const dispatch = createEventDispatcher<{
close: void;
refresh: void;
}>();
async function loadPeople() {
const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
try {
@@ -85,7 +82,7 @@
) {
clearTimeout(loaderLoadingDoneTimeout);
clearTimeout(automaticRefreshTimeout);
dispatch('refresh');
onRefresh();
}
};
@@ -98,10 +95,6 @@
return b.every((valueB) => a.includes(valueB));
};
const handleBackButton = () => {
dispatch('close');
};
const handleReset = (id: string) => {
if (selectedPersonToReassign[id]) {
delete selectedPersonToReassign[id];
@@ -153,9 +146,9 @@
isShowLoadingDone = false;
if (peopleToCreate.length === 0) {
clearTimeout(loaderLoadingDoneTimeout);
dispatch('refresh');
onRefresh();
} else {
automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15_000);
automaticRefreshTimeout = setTimeout(onRefresh, 15_000);
}
};
@@ -185,7 +178,7 @@
>
<div class="flex place-items-center justify-between gap-2">
<div class="flex items-center gap-2">
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('edit_faces')}</p>
</div>
{#if !isShowLoadingDone}
@@ -336,8 +329,8 @@
{editedFace}
{assetId}
{assetType}
on:close={() => (showSelectedFaces = false)}
on:createPerson={(event) => handleCreatePerson(event.detail)}
on:reassign={(event) => handleReassignFace(event.detail)}
onClose={() => (showSelectedFaces = false)}
onCreatePerson={handleCreatePerson}
onReassign={handleReassignFace}
/>
{/if}
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiCake } from '@mdi/js';
@@ -7,28 +6,20 @@
import { t } from 'svelte-i18n';
export let birthDate: string;
const dispatch = createEventDispatcher<{
close: void;
updated: string;
}>();
export let onClose: () => void;
export let onUpdate: (birthDate: string) => void;
const todayFormatted = new Date().toISOString().split('T')[0];
const handleCancel = () => dispatch('close');
const handleSubmit = () => {
dispatch('updated', birthDate);
};
</script>
<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}>
<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} {onClose}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg">
{$t('birthdate_set_description')}
</p>
</div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="set-birth-date-form">
<form on:submit|preventDefault={() => onUpdate(birthDate)} autocomplete="off" id="set-birth-date-form">
<div class="my-4 flex flex-col gap-2">
<DateInput
class="immich-form-input"
@@ -41,7 +32,7 @@
</div>
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
<Button color="gray" fullwidth on:click={onClose}>{$t('cancel')}</Button>
<Button type="submit" fullwidth form="set-birth-date-form">{$t('set')}</Button>
</svelte:fragment>
</FullScreenModal>
@@ -10,7 +10,7 @@
type PersonResponseDto,
} from '@immich/sdk';
import { mdiMerge, mdiPlus } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import Button from '../elements/buttons/button.svelte';
@@ -23,6 +23,8 @@
export let assetIds: string[];
export let personAssets: PersonResponseDto;
export let onConfirm: () => void;
export let onClose: () => void;
let people: PersonResponseDto[] = [];
let selectedPerson: PersonResponseDto | null = null;
@@ -34,11 +36,6 @@
$: peopleToNotShow = selectedPerson ? [personAssets, selectedPerson] : [personAssets];
let dispatch = createEventDispatcher<{
confirm: void;
close: void;
}>();
const selectedPeople: AssetFaceUpdateItem[] = [];
for (const assetId of assetIds) {
@@ -50,10 +47,6 @@
people = data.people;
});
const onClose = () => {
dispatch('close');
};
const handleSelectedPerson = (person: PersonResponseDto) => {
if (selectedPerson && selectedPerson.id === person.id) {
handleRemoveSelectedPerson();
@@ -87,7 +80,7 @@
}
showLoadingSpinnerCreate = false;
dispatch('confirm');
onConfirm();
};
const handleReassign = async () => {
@@ -113,7 +106,7 @@
}
showLoadingSpinnerReassign = false;
dispatch('confirm');
onConfirm();
};
</script>
@@ -123,7 +116,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
>
<ControlAppBar on:close={onClose}>
<ControlAppBar {onClose}>
<svelte:fragment slot="leading">
<slot name="header" />
<div />
@@ -175,12 +168,12 @@
circle
selectable
thumbnailSize={180}
on:click={handleRemoveSelectedPerson}
onClick={handleRemoveSelectedPerson}
/>
</div>
</div>
{/if}
<PeopleList {people} {peopleToNotShow} {screenHeight} on:select={({ detail }) => handleSelectedPerson(detail)} />
<PeopleList {people} {peopleToNotShow} {screenHeight} onSelect={handleSelectedPerson} />
</section>
</section>
</section>
@@ -1,20 +1,15 @@
<script lang="ts">
import { copyToClipboard } from '$lib/utils';
import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { t } from 'svelte-i18n';
export let secret = '';
const dispatch = createEventDispatcher<{
done: void;
}>();
const handleDone = () => dispatch('done');
export let onDone: () => void;
</script>
<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}>
<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={onDone}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg">
{$t('api_key_description')}
@@ -28,6 +23,6 @@
<svelte:fragment slot="sticky-bottom">
<Button on:click={() => copyToClipboard(secret)} fullwidth>{$t('copy_to_clipboard')}</Button>
<Button on:click={() => handleDone()} fullwidth>{$t('done')}</Button>
<Button on:click={onDone} fullwidth>{$t('done')}</Button>
</svelte:fragment>
</FullScreenModal>
@@ -1,10 +1,11 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import PasswordField from '../shared-components/password-field.svelte';
import { updateMyUser } from '@immich/sdk';
import { t } from 'svelte-i18n';
export let onSuccess: () => void;
let errorMessage: string;
let success: string;
@@ -23,17 +24,13 @@
}
}
const dispatch = createEventDispatcher<{
success: void;
}>();
async function changePassword() {
if (valid) {
errorMessage = '';
await updateMyUser({ userUpdateMeDto: { password: String(password) } });
dispatch('success');
onSuccess();
}
}
</script>
@@ -1,17 +1,18 @@
<script lang="ts">
import { serverInfo } from '$lib/stores/server-info.store';
import { handleError } from '$lib/utils/handle-error';
import { createUserAdmin } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import PasswordField from '../shared-components/password-field.svelte';
import Slider from '../elements/slider.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { t } from 'svelte-i18n';
import { serverInfo } from '$lib/stores/server-info.store';
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { createUserAdmin } from '@immich/sdk';
import { t } from 'svelte-i18n';
import Button from '../elements/buttons/button.svelte';
import Slider from '../elements/slider.svelte';
import PasswordField from '../shared-components/password-field.svelte';
export let onClose: () => void;
export let onSubmit: () => void;
export let onCancel: () => void;
let error: string;
let success: string;
@@ -39,10 +40,6 @@
canCreateUser = true;
}
}
const dispatch = createEventDispatcher<{
submit: void;
cancel: void;
}>();
async function registerUser() {
if (canCreateUser && !isCreatingUser) {
@@ -63,7 +60,7 @@
success = $t('new_user_created');
dispatch('submit');
onSubmit();
return;
} catch (error) {
@@ -132,7 +129,7 @@
{/if}
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>{$t('cancel')}</Button>
<Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button>
</svelte:fragment>
</FullScreenModal>
@@ -5,7 +5,6 @@
import { handleError } from '$lib/utils/handle-error';
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
import { mdiAccountEditOutline } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
@@ -15,6 +14,8 @@
export let canResetPassword = true;
export let newPassword: string;
export let onClose: () => void;
export let onResetPasswordSuccess: () => void;
export let onEditSuccess: () => void;
let error: string;
let success: string;
@@ -27,12 +28,6 @@
!!quotaSize &&
convertToBytes(Number(quotaSize), ByteUnit.GiB) > $serverInfo.diskSizeRaw;
const dispatch = createEventDispatcher<{
close: void;
resetPasswordSuccess: void;
editSuccess: void;
}>();
const editUser = async () => {
try {
const { id, email, name, storageLabel } = user;
@@ -46,7 +41,7 @@
},
});
dispatch('editSuccess');
onEditSuccess();
} catch (error) {
handleError(error, $t('errors.unable_to_update_user'));
}
@@ -72,7 +67,7 @@
},
});
dispatch('resetPasswordSuccess');
onResetPasswordSuccess();
} catch (error) {
handleError(error, $t('errors.unable_to_reset_password'));
}
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderRemove } from '@mdi/js';
@@ -10,6 +9,9 @@
export let exclusionPatterns: string[] = [];
export let isEditing = false;
export let submitText = $t('submit');
export let onCancel: () => void;
export let onSubmit: (exclusionPattern: string) => void;
export let onDelete: () => void = () => {};
onMount(() => {
if (isEditing) {
@@ -19,18 +21,10 @@
$: isDuplicate = exclusionPattern !== null && exclusionPatterns.includes(exclusionPattern);
$: canSubmit = exclusionPattern && !exclusionPatterns.includes(exclusionPattern);
const dispatch = createEventDispatcher<{
cancel: void;
submit: { excludePattern: string };
delete: void;
}>();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
</script>
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={handleCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}>
<form on:submit|preventDefault={() => onSubmit(exclusionPattern)} autocomplete="off" id="add-exclusion-pattern-form">
<p class="py-5 text-sm">
{$t('admin.exclusion_pattern_description')}
<br /><br />
@@ -53,9 +47,9 @@
</div>
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
<Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button>
<Button color="red" fullwidth on:click={onDelete}>{$t('delete')}</Button>
{/if}
<Button type="submit" disabled={!canSubmit} fullwidth form="add-exclusion-pattern-form">{submitText}</Button>
</svelte:fragment>
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderSync } from '@mdi/js';
@@ -12,6 +11,9 @@
export let cancelText = $t('cancel');
export let submitText = $t('save');
export let isEditing = false;
export let onCancel: () => void;
export let onSubmit: (importPath: string | null) => void;
export let onDelete: () => void = () => {};
onMount(() => {
if (isEditing) {
@@ -21,18 +23,10 @@
$: isDuplicate = importPath !== null && importPaths.includes(importPath);
$: canSubmit = importPath !== '' && importPath !== null && !importPaths.includes(importPath);
const dispatch = createEventDispatcher<{
cancel: void;
submit: { importPath: string | null };
delete: void;
}>();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { importPath });
</script>
<FullScreenModal {title} icon={mdiFolderSync} onClose={handleCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="library-import-path-form">
<FullScreenModal {title} icon={mdiFolderSync} onClose={onCancel}>
<form on:submit|preventDefault={() => onSubmit(importPath)} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
<div class="my-4 flex flex-col gap-2">
@@ -47,9 +41,9 @@
</div>
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
<Button color="gray" fullwidth on:click={onCancel}>{cancelText}</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button>
<Button color="red" fullwidth on:click={onDelete}>{$t('delete')}</Button>
{/if}
<Button type="submit" disabled={!canSubmit} fullwidth form="library-import-path-form">{submitText}</Button>
</svelte:fragment>
@@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte';
import LibraryImportPathForm from './library-import-path-form.svelte';
@@ -12,6 +12,8 @@
import { t } from 'svelte-i18n';
export let library: LibraryResponseDto;
export let onCancel: () => void;
export let onSubmit: (library: LibraryResponseDto) => void;
let addImportPath = false;
let editImportPath: number | null = null;
@@ -65,19 +67,6 @@
}
};
const dispatch = createEventDispatcher<{
cancel: void;
submit: Partial<LibraryResponseDto>;
}>();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', { ...library });
};
const handleAddImportPath = async () => {
if (!addImportPath || !importPathToAdd) {
return;
@@ -153,8 +142,8 @@
submitText={$t('add')}
bind:importPath={importPathToAdd}
{importPaths}
on:submit={handleAddImportPath}
on:cancel={() => {
onSubmit={handleAddImportPath}
onCancel={() => {
addImportPath = false;
importPathToAdd = null;
}}
@@ -168,15 +157,13 @@
isEditing={true}
bind:importPath={editedImportPath}
{importPaths}
on:submit={handleEditImportPath}
on:delete={handleDeleteImportPath}
on:cancel={() => {
editImportPath = null;
}}
onSubmit={handleEditImportPath}
onDelete={handleDeleteImportPath}
onCancel={() => (editImportPath = null)}
/>
{/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4">
<form on:submit|preventDefault={() => onSubmit({ ...library })} autocomplete="off" class="m-4 flex flex-col gap-4">
<table class="text-left">
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each validatedPaths as validatedPath, listIndex}
@@ -251,7 +238,7 @@
>
</div>
<div class="justify-end gap-2">
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
<Button size="sm" color="gray" on:click={onCancel}>{$t('cancel')}</Button>
<Button size="sm" type="submit">{$t('save')}</Button>
</div>
</div>
@@ -1,31 +1,20 @@
<script lang="ts">
import type { LibraryResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import { t } from 'svelte-i18n';
export let library: Partial<LibraryResponseDto>;
const dispatch = createEventDispatcher<{
cancel: void;
submit: Partial<LibraryResponseDto>;
}>();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', { ...library });
};
export let onCancel: () => void;
export let onSubmit: (library: Partial<LibraryResponseDto>) => void;
</script>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-2">
<form on:submit|preventDefault={() => onSubmit({ ...library })} autocomplete="off" class="m-4 flex flex-col gap-2">
<div class="flex flex-col gap-2">
<label class="immich-form-label" for="path">{$t('name')}</label>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={library.name} />
</div>
<div class="flex w-full justify-end gap-2 pt-2">
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
<Button size="sm" color="gray" on:click={onCancel}>{$t('cancel')}</Button>
<Button size="sm" type="submit">{$t('save')}</Button>
</div>
</form>
@@ -1,7 +1,7 @@
<script lang="ts">
import { type LibraryResponseDto } from '@immich/sdk';
import { mdiPencilOutline } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte';
import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
@@ -9,6 +9,8 @@
import { t } from 'svelte-i18n';
export let library: Partial<LibraryResponseDto>;
export let onCancel: () => void;
export let onSubmit: (library: Partial<LibraryResponseDto>) => void;
let addExclusionPattern = false;
let editExclusionPattern: number | null = null;
@@ -26,18 +28,6 @@
}
});
const dispatch = createEventDispatcher<{
cancel: void;
submit: Partial<LibraryResponseDto>;
}>();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', library);
};
const handleAddExclusionPattern = () => {
if (!addExclusionPattern) {
return;
@@ -106,10 +96,8 @@
submitText={$t('add')}
bind:exclusionPattern={exclusionPatternToAdd}
{exclusionPatterns}
on:submit={handleAddExclusionPattern}
on:cancel={() => {
addExclusionPattern = false;
}}
onSubmit={handleAddExclusionPattern}
onCancel={() => (addExclusionPattern = false)}
/>
{/if}
@@ -119,15 +107,13 @@
isEditing={true}
bind:exclusionPattern={editedExclusionPattern}
{exclusionPatterns}
on:submit={handleEditExclusionPattern}
on:delete={handleDeleteExclusionPattern}
on:cancel={() => {
editExclusionPattern = null;
}}
onSubmit={handleEditExclusionPattern}
onDelete={handleDeleteExclusionPattern}
onCancel={() => (editExclusionPattern = null)}
/>
{/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4">
<form on:submit|preventDefault={() => onSubmit(library)} autocomplete="off" class="m-4 flex flex-col gap-4">
<table class="w-full text-left">
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each exclusionPatterns as exclusionPattern, listIndex}
@@ -178,7 +164,7 @@
</table>
<div class="flex w-full justify-end gap-4">
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button>
<Button size="sm" color="gray" on:click={onCancel}>{$t('cancel')}</Button>
<Button size="sm" type="submit">{$t('save')}</Button>
</div>
</form>
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderSync } from '@mdi/js';
@@ -9,6 +8,9 @@
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { t } from 'svelte-i18n';
export let onCancel: () => void;
export let onSubmit: (ownerId: string) => void;
let ownerId: string = $user.id;
let userOptions: { value: string; text: string }[] = [];
@@ -17,25 +19,16 @@
const users = await searchUsersAdmin({});
userOptions = users.map((user) => ({ value: user.id, text: user.name }));
});
const dispatch = createEventDispatcher<{
cancel: void;
submit: { ownerId: string };
delete: void;
}>();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { ownerId });
</script>
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={handleCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel}>
<form on:submit|preventDefault={() => onSubmit(ownerId)} autocomplete="off" id="select-library-owner-form">
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button>
<Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
<Button type="submit" fullwidth form="select-library-owner-form">{$t('create')}</Button>
</svelte:fragment>
</FullScreenModal>
@@ -52,7 +52,7 @@
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<Combobox
on:select={({ detail: option }) => handleSelect(option)}
onSelect={handleSelect}
label={$t('tag')}
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
@@ -21,7 +21,7 @@
<header>
{#if !hideNavbar}
<NavigationBar {showUploadButton} on:uploadClicked={() => openFileUploadDialog()} />
<NavigationBar {showUploadButton} onUploadClick={() => openFileUploadDialog()} />
{/if}
<slot name="header" />
@@ -4,7 +4,6 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import type { MapSettings } from '$lib/stores/preferences.store';
import { Duration } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
import Button from '../elements/buttons/button.svelte';
@@ -12,19 +11,15 @@
import DateInput from '../elements/date-input.svelte';
export let settings: MapSettings;
export let onClose: () => void;
export let onSave: (settings: MapSettings) => void;
let customDateRange = !!settings.dateAfter || !!settings.dateBefore;
const dispatch = createEventDispatcher<{
close: void;
save: MapSettings;
}>();
const handleClose = () => dispatch('close');
</script>
<FullScreenModal title={$t('map_settings')} onClose={handleClose}>
<FullScreenModal title={$t('map_settings')} {onClose}>
<form
on:submit|preventDefault={() => dispatch('save', settings)}
on:submit|preventDefault={() => onSave(settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
id="map-settings-form"
>
@@ -108,7 +103,7 @@
{/if}
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" size="sm" fullwidth on:click={handleClose}>{$t('cancel')}</Button>
<Button color="gray" size="sm" fullwidth on:click={onClose}>{$t('cancel')}</Button>
<Button type="submit" size="sm" fullwidth form="map-settings-form">{$t('save')}</Button>
</svelte:fragment>
</FullScreenModal>
@@ -250,7 +250,7 @@
<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
{#if current && current.memory.assets.length > 0}
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
<ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} forceDark>
<svelte:fragment slot="leading">
<p class="text-lg">
{$memoryLaneTitle(current.memory.yearsAgo)}
@@ -40,8 +40,8 @@
{#if showAlbumPicker}
<AlbumSelectionModal
{shared}
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
on:album={({ detail }) => handleAddToAlbum(detail)}
onNewAlbum={handleAddToNewAlbum}
onAlbumClick={handleAddToAlbum}
onClose={handleHideAlbumPicker}
/>
{/if}
@@ -31,9 +31,5 @@
<MenuOption text={$t('change_date')} icon={mdiCalendarEditOutline} onClick={() => (isShowChangeDate = true)} />
{/if}
{#if isShowChangeDate}
<ChangeDate
initialDate={DateTime.now()}
on:confirm={({ detail: date }) => handleConfirm(date)}
on:cancel={() => (isShowChangeDate = false)}
/>
<ChangeDate initialDate={DateTime.now()} onConfirm={handleConfirm} onCancel={() => (isShowChangeDate = false)} />
{/if}
@@ -35,8 +35,5 @@
/>
{/if}
{#if isShowChangeLocation}
<ChangeLocation
on:confirm={({ detail: point }) => handleConfirm(point)}
on:cancel={() => (isShowChangeLocation = false)}
/>
<ChangeLocation onConfirm={handleConfirm} onCancel={() => (isShowChangeLocation = false)} />
{/if}
@@ -49,7 +49,7 @@
{#if isShowConfirmation}
<DeleteAssetDialog
size={getOwnedAssets().size}
on:confirm={handleDelete}
on:cancel={() => (isShowConfirmation = false)}
onConfirm={handleDelete}
onCancel={() => (isShowConfirmation = false)}
/>
{/if}
@@ -8,7 +8,7 @@
import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk';
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
import { createEventDispatcher, onDestroy } from 'svelte';
import { onDestroy } from 'svelte';
import { fly } from 'svelte/transition';
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
import { TUNABLES } from '$lib/utils/tunables';
@@ -29,6 +29,9 @@
export let onScrollTarget: ScrollTargetListener | undefined = undefined;
export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined;
export let onSelect: ({ title, assets }: { title: string; assets: AssetResponseDto[] }) => void;
export let onSelectAssets: (asset: AssetResponseDto) => void;
export let onSelectAssetCandidates: (asset: AssetResponseDto | null) => void;
const componentId = generateId();
$: bucketDate = bucket.bucketDate;
@@ -41,11 +44,6 @@
const TITLE_HEIGHT = 51;
const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore;
const dispatch = createEventDispatcher<{
select: { title: string; assets: AssetResponseDto[] };
selectAssets: AssetResponseDto;
selectAssetCandidates: AssetResponseDto | null;
}>();
let isMouseOverGroup = false;
let hoveredDateGroup = '';
@@ -65,10 +63,10 @@
}
};
const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets });
const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => onSelect({ title, assets });
const assetSelectHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => {
dispatch('selectAssets', asset);
onSelectAssets(asset);
// Check if all assets are selected in a group to toggle the group selection's icon
let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length;
@@ -86,7 +84,7 @@
hoveredDateGroup = groupTitle;
if ($isMultiSelectState) {
dispatch('selectAssetCandidates', asset);
onSelectAssetCandidates(asset);
}
};
@@ -28,7 +28,7 @@
import { TUNABLES } from '$lib/utils/tunables';
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
import { throttle } from 'lodash-es';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import Portal from '../shared-components/portal/portal.svelte';
import Scrubber from '../shared-components/scrubber/scrubber.svelte';
import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
@@ -64,6 +64,8 @@
export let isShared = false;
export let album: AlbumResponseDto | null = null;
export let isShowDeleteConfirmation = false;
export let onSelect: (asset: AssetResponseDto) => void = () => {};
export let onEscape: () => void = () => {};
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } =
@@ -127,8 +129,6 @@
},
} = TUNABLES;
const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
const isViewportOrigin = () => {
return viewport.height === 0 && viewport.width === 0;
};
@@ -447,7 +447,7 @@
const ids = await stackAssets(Array.from($selectedAssets));
if (ids) {
$assetStore.removeAssets(ids);
dispatch('escape');
onEscape();
}
};
@@ -471,7 +471,7 @@
}
const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: 'Escape' }, onShortcut: () => dispatch('escape') },
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) },
@@ -539,7 +539,7 @@
return !!nextAsset;
};
const handleClose = async ({ detail: { asset } }: { detail: { asset: AssetResponseDto } }) => {
const handleClose = async ({ asset }: { asset: AssetResponseDto }) => {
assetViewingStore.showAssetViewer(false);
showSkeleton = true;
$gridScrollTarget = { at: asset.id };
@@ -554,7 +554,7 @@
case AssetAction.DELETE: {
// find the next asset to show or close the viewer
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
(await handleNext()) || (await handlePrevious()) || (await handleClose({ detail: { asset: action.asset } }));
(await handleNext()) || (await handlePrevious()) || (await handleClose({ asset: action.asset }));
// delete after find the next one
assetStore.removeAssets([action.asset.id]);
@@ -649,7 +649,7 @@
return;
}
dispatch('select', asset);
onSelect(asset);
if (singleSelect) {
element.scrollTop = 0;
@@ -754,13 +754,13 @@
{#if isShowDeleteConfirmation}
<DeleteAssetDialog
size={idsSelectedAssets.length}
on:cancel={() => (isShowDeleteConfirmation = false)}
on:confirm={() => handlePromiseError(trashOrDelete(true))}
onCancel={() => (isShowDeleteConfirmation = false)}
onConfirm={() => handlePromiseError(trashOrDelete(true))}
/>
{/if}
{#if showShortcuts}
<ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} />
<ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} />
{/if}
{#if assetStore.buckets.length > 0}
<Scrubber
@@ -847,9 +847,9 @@
{onAssetInGrid}
{bucket}
viewport={safeViewport}
on:select={({ detail: group }) => handleGroupSelect(group.title, group.assets)}
on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)}
on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)}
onSelect={({ title, assets }) => handleGroupSelect(title, assets)}
onSelectAssetCandidates={handleSelectAssetCandidates}
onSelectAssets={handleSelectAssets}
/>
{/if}
</div>
@@ -869,9 +869,9 @@
{isShared}
{album}
onAction={handleAction}
on:previous={handlePrevious}
on:next={handleNext}
on:close={handleClose}
onPrevious={handlePrevious}
onNext={handleNext}
onClose={handleClose}
/>
{/await}
{/if}
@@ -30,7 +30,7 @@
});
</script>
<ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
<ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
<div class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
<p class="block sm:hidden">{assets.size}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p>
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
import Checkbox from '$lib/components/elements/checkbox.svelte';
@@ -7,19 +6,16 @@
import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let size: number;
export let onConfirm: () => void;
export let onCancel: () => void;
let checked = false;
const dispatch = createEventDispatcher<{
confirm: void;
cancel: void;
}>();
const handleConfirm = () => {
if (checked) {
$showDeleteModal = false;
}
dispatch('confirm');
onConfirm();
};
</script>
@@ -27,7 +23,7 @@
title={$t('permanently_delete_assets_count', { values: { count: size } })}
confirmText={$t('delete')}
onConfirm={handleConfirm}
onCancel={() => dispatch('cancel')}
{onCancel}
>
<svelte:fragment slot="prompt">
<p>
@@ -84,7 +84,7 @@
{/if}
</AssetSelectControlBar>
{:else}
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}>
<ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}>
<svelte:fragment slot="leading">
<ImmichLogoSmallLink width={innerWidth} />
</svelte:fragment>
@@ -2,7 +2,7 @@
import Icon from '$lib/components/elements/icon.svelte';
import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
import { mdiPlus } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import AlbumListItem from '../asset-viewer/album-list-item.svelte';
import { normalizeSearchString } from '$lib/utils/string-utils';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
@@ -11,17 +11,15 @@
import { sortAlbums } from '$lib/utils/album-utils';
import { albumViewSettings } from '$lib/stores/preferences.store';
export let onNewAlbum: (search: string) => void;
export let onAlbumClick: (album: AlbumResponseDto) => void;
let albums: AlbumResponseDto[] = [];
let recentAlbums: AlbumResponseDto[] = [];
let filteredAlbums: AlbumResponseDto[] = [];
let loading = true;
let search = '';
const dispatch = createEventDispatcher<{
newAlbum: string;
album: AlbumResponseDto;
}>();
export let shared: boolean;
export let onClose: () => void;
@@ -40,14 +38,6 @@
{ sortBy: $albumViewSettings.sortBy, orderBy: $albumViewSettings.sortOrder },
);
const handleSelect = (album: AlbumResponseDto) => {
dispatch('album', album);
};
const handleNew = () => {
dispatch('newAlbum', search.length > 0 ? search : '');
};
const getTitle = () => {
if (shared) {
return $t('add_to_shared_album');
@@ -81,7 +71,7 @@
<div class="immich-scrollbar overflow-y-auto">
<button
type="button"
on:click={handleNew}
on:click={() => onNewAlbum(search)}
class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
>
<div class="flex h-12 w-12 items-center justify-center">
@@ -96,7 +86,7 @@
{#if !shared && search.length === 0}
<p class="px-5 py-3 text-xs">{$t('recent').toUpperCase()}</p>
{#each recentAlbums as album (album.id)}
<AlbumListItem {album} on:album={() => handleSelect(album)} />
<AlbumListItem {album} onAlbumClick={() => onAlbumClick(album)} />
{/each}
{/if}
@@ -106,7 +96,7 @@
</p>
{/if}
{#each filteredAlbums as album (album.id)}
<AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} />
<AlbumListItem {album} searchQuery={search} onAlbumClick={() => onAlbumClick(album)} />
{/each}
{:else if albums.length > 0}
<p class="px-5 py-1 text-sm">{$t('no_albums_with_name_yet')}</p>
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { DateTime } from 'luxon';
import ConfirmDialog from './dialog/confirm-dialog.svelte';
import Combobox from './combobox.svelte';
@@ -8,6 +7,8 @@
export let initialDate: DateTime = DateTime.now();
export let initialTimeZone: string = '';
export let onCancel: () => void;
export let onConfirm: (date: string) => void;
type ZoneOption = {
/**
@@ -118,17 +119,10 @@
return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' });
}
const dispatch = createEventDispatcher<{
cancel: void;
confirm: string;
}>();
const handleCancel = () => dispatch('cancel');
const handleConfirm = () => {
const value = date.toISO();
if (value) {
dispatch('confirm', value);
onConfirm(value);
}
};
</script>
@@ -139,7 +133,7 @@
prompt="Please select a new date:"
disabled={!date.isValid}
onConfirm={handleConfirm}
onCancel={handleCancel}
{onCancel}
>
<div class="flex flex-col text-left gap-2" slot="prompt">
<div class="flex flex-col">
@@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import ConfirmDialog from './dialog/confirm-dialog.svelte';
import { timeDebounceOnSearch } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
@@ -14,13 +13,15 @@
import { t } from 'svelte-i18n';
import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte';
export let asset: AssetResponseDto | undefined = undefined;
interface Point {
lng: number;
lat: number;
}
export let asset: AssetResponseDto | undefined = undefined;
export let onCancel: () => void;
export let onConfirm: (point: Point) => void;
let places: PlacesResponseDto[] = [];
let suggestedPlaces: PlacesResponseDto[] = [];
let searchWord: string;
@@ -30,11 +31,6 @@
let hideSuggestion = false;
let addClipMapMarker: (long: number, lat: number) => void;
const dispatch = createEventDispatcher<{
cancel: void;
confirm: Point;
}>();
$: lat = asset?.exifInfo?.latitude ?? undefined;
$: lng = asset?.exifInfo?.longitude ?? undefined;
$: zoom = lat !== undefined && lng !== undefined ? 12.5 : 1;
@@ -50,17 +46,11 @@
let point: Point | null = null;
const handleCancel = () => dispatch('cancel');
const handleSelect = (selected: Point) => {
point = selected;
};
const handleConfirm = () => {
if (point) {
dispatch('confirm', point);
onConfirm(point);
} else {
dispatch('cancel');
onCancel();
}
};
@@ -108,13 +98,7 @@
};
</script>
<ConfirmDialog
confirmColor="primary"
title={$t('change_location')}
width="wide"
onConfirm={handleConfirm}
onCancel={handleCancel}
>
<ConfirmDialog confirmColor="primary" title={$t('change_location')} width="wide" onConfirm={handleConfirm} {onCancel}>
<div slot="prompt" class="flex flex-col w-full h-full gap-2">
<div
class="relative w-64 sm:w-96"
@@ -126,10 +110,8 @@
placeholder={$t('search_places')}
bind:name={searchWord}
{showLoadingSpinner}
on:reset={() => {
suggestedPlaces = [];
}}
on:search={handleSearchPlaces}
onReset={() => (suggestedPlaces = [])}
onSearch={handleSearchPlaces}
roundedBottom={suggestedPlaces.length === 0 || hideSuggestion}
/>
</button>
@@ -180,7 +162,7 @@
center={lat && lng ? { lat, lng } : undefined}
simplified={true}
clickable={true}
on:clickedPoint={({ detail: point }) => handleSelect(point)}
onClickPoint={(selected) => (point = selected)}
/>
{/await}
</div>
@@ -21,7 +21,7 @@
import { fly } from 'svelte/transition';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js';
import { createEventDispatcher, tick } from 'svelte';
import { tick } from 'svelte';
import type { FormEventHandler } from 'svelte/elements';
import { shortcuts } from '$lib/actions/shortcut';
import { focusOutside } from '$lib/actions/focus-outside';
@@ -35,6 +35,7 @@
export let options: ComboBoxOption[] = [];
export let selectedOption: ComboBoxOption | undefined = undefined;
export let placeholder = '';
export let onSelect: (option: ComboBoxOption | undefined) => void = () => {};
/**
* Unique identifier for the combobox.
@@ -61,10 +62,6 @@
searchQuery = selectedOption ? selectedOption.label : '';
}
const dispatch = createEventDispatcher<{
select: ComboBoxOption | undefined;
}>();
const activate = () => {
isActive = true;
searchQuery = '';
@@ -105,10 +102,10 @@
optionRefs[0]?.scrollIntoView({ block: 'nearest' });
};
let onSelect = (option: ComboBoxOption) => {
let handleSelect = (option: ComboBoxOption) => {
selectedOption = option;
searchQuery = option.label;
dispatch('select', option);
onSelect(option);
closeDropdown();
};
@@ -117,7 +114,7 @@
selectedIndex = undefined;
selectedOption = undefined;
searchQuery = '';
dispatch('select', selectedOption);
onSelect(selectedOption);
};
</script>
@@ -188,7 +185,7 @@
shortcut: { key: 'Enter' },
onShortcut: () => {
if (selectedIndex !== undefined && filteredOptions.length > 0) {
onSelect(filteredOptions[selectedIndex]);
handleSelect(filteredOptions[selectedIndex]);
}
closeDropdown();
},
@@ -220,7 +217,7 @@
role="listbox"
id={listboxId}
transition:fly={{ duration: 250 }}
class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-10"
class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-[10000]"
class:border={isOpen}
tabindex="-1"
>
@@ -245,7 +242,7 @@
bind:this={optionRefs[index]}
class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700"
id={`${listboxId}-${index}`}
on:click={() => onSelect(option)}
on:click={() => handleSelect(option)}
role="option"
>
{option.label}
@@ -1,7 +1,7 @@
<script lang="ts">
import { browser } from '$app/environment';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { fly } from 'svelte/transition';
import { mdiClose } from '@mdi/js';
@@ -12,13 +12,10 @@
export let backIcon = mdiClose;
export let tailwindClasses = '';
export let forceDark = false;
export let onClose: () => void = () => {};
let appBarBorder = 'bg-immich-bg border border-transparent';
const dispatch = createEventDispatcher<{
close: void;
}>();
const onScroll = () => {
if (window.pageYOffset > 80) {
appBarBorder = 'border border-gray-200 bg-gray-50 dark:border-gray-600';
@@ -33,7 +30,7 @@
const handleClose = () => {
$isSelectingAllAssets = false;
dispatch('close');
onClose();
};
onMount(() => {
@@ -7,7 +7,6 @@
import { handleError } from '$lib/utils/handle-error';
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
import { mdiContentCopy, mdiLink } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { NotificationType, notificationController } from '../notification/notification';
import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte';
import SettingSwitch from '../settings/setting-switch.svelte';
@@ -21,6 +20,7 @@
export let albumId: string | undefined = undefined;
export let assetIds: string[] = [];
export let editingLink: SharedLinkResponseDto | undefined = undefined;
export let onCreated: () => void = () => {};
let sharedLink: string | null = null;
let description = '';
@@ -32,10 +32,6 @@
let shouldChangeExpirationTime = false;
let enablePassword = false;
const dispatch = createEventDispatcher<{
created: void;
}>();
const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
[30, 'minutes'],
[1, 'hour'],
@@ -97,7 +93,7 @@
},
});
sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
dispatch('created');
onCreated();
} catch (error) {
handleError(error, $t('errors.failed_to_create_shared_link'));
}
@@ -163,9 +163,9 @@
<AssetViewer
asset={$viewingAsset}
onAction={handleAction}
on:previous={handlePrevious}
on:next={handleNext}
on:close={() => {
onPrevious={handlePrevious}
onNext={handleNext}
onClose={() => {
assetViewingStore.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}}
@@ -6,14 +6,13 @@
import Icon from '$lib/components/elements/icon.svelte';
import { Theme } from '$lib/constants';
import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
import { getAssetThumbnailUrl, getKey, handlePromiseError } from '$lib/utils';
import { getMapStyle, MapTheme, type MapMarkerResponseDto } from '@immich/sdk';
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url';
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
import { getServerConfig, type MapMarkerResponseDto } from '@immich/sdk';
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text?url';
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
import type { GeoJSONSource, LngLatLike, StyleSpecification } from 'maplibre-gl';
import maplibregl from 'maplibre-gl';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import {
AttributionControl,
@@ -52,26 +51,25 @@
}
export let onOpenInMapView: (() => Promise<void> | void) | undefined = undefined;
export let onSelect: (assetIds: string[]) => void = () => {};
export let onClickPoint: ({ lat, lng }: { lat: number; lng: number }) => void = () => {};
let map: maplibregl.Map;
let marker: maplibregl.Marker | null = null;
$: style = (() =>
getMapStyle({
theme: ($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT) as unknown as MapTheme,
key: getKey(),
}) as Promise<StyleSpecification>)();
const dispatch = createEventDispatcher<{
selected: string[];
clickedPoint: { lat: number; lng: number };
}>();
$: style = (async () => {
const config = await getServerConfig();
const theme = $mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT;
const styleUrl = theme === Theme.DARK ? config.mapDarkStyleUrl : config.mapLightStyleUrl;
const style = await fetch(styleUrl).then((response) => response.json());
return style as StyleSpecification;
})();
function handleAssetClick(assetId: string, map: Map | null) {
if (!map) {
return;
}
dispatch('selected', [assetId]);
onSelect([assetId]);
}
async function handleClusterClick(clusterId: number, map: Map | null) {
@@ -82,13 +80,13 @@
const mapSource = map?.getSource('geojson') as GeoJSONSource;
const leaves = await mapSource.getClusterLeaves(clusterId, 10_000, 0);
const ids = leaves.map((leaf) => leaf.properties?.id);
dispatch('selected', ids);
onSelect(ids);
}
function handleMapClick(event: maplibregl.MapMouseEvent) {
if (clickable) {
const { lng, lat } = event.lngLat;
dispatch('clickedPoint', { lng, lat });
onClickPoint({ lng, lat });
if (marker) {
marker.remove();
@@ -1,4 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { focusTrap } from '$lib/actions/focus-trap';
import Button from '$lib/components/elements/buttons/button.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
@@ -8,21 +9,17 @@
import { handleError } from '$lib/utils/handle-error';
import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk';
import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import { NotificationType, notificationController } from '../notification/notification';
import UserAvatar from '../user-avatar.svelte';
import AvatarSelector from './avatar-selector.svelte';
import { t } from 'svelte-i18n';
import { page } from '$app/stores';
export let onLogout: () => void;
export let onClose: () => void = () => {};
let isShowSelectAvatar = false;
const dispatch = createEventDispatcher<{
logout: void;
close: void;
}>();
const handleSaveProfile = async (color: UserAvatarColor) => {
try {
if ($user.profileImagePath !== '') {
@@ -75,14 +72,7 @@
</div>
<div class="flex flex-col gap-1">
<Button
href={AppRoute.USER_SETTINGS}
on:click={() => dispatch('close')}
color="dark-gray"
size="sm"
shadow={false}
border
>
<Button href={AppRoute.USER_SETTINGS} on:click={onClose} color="dark-gray" size="sm" shadow={false} border>
<div class="flex place-content-center place-items-center text-center gap-2 px-2">
<Icon path={mdiCog} size="18" ariaHidden />
{$t('account_settings')}
@@ -91,7 +81,7 @@
{#if $user.isAdmin}
<Button
href={AppRoute.ADMIN_USER_MANAGEMENT}
on:click={() => dispatch('close')}
on:click={onClose}
color="dark-gray"
size="sm"
shadow={false}
@@ -111,7 +101,7 @@
<button
type="button"
class="flex w-full place-content-center place-items-center gap-2 py-3 font-medium text-gray-500 hover:bg-immich-primary/10 dark:text-gray-300"
on:click={() => dispatch('logout')}
on:click={onLogout}
>
<Icon path={mdiLogout} size={24} />
{$t('sign_out')}</button
@@ -120,9 +110,5 @@
</div>
{#if isShowSelectAvatar}
<AvatarSelector
user={$user}
on:close={() => (isShowSelectAvatar = false)}
on:choose={({ detail: color }) => handleSaveProfile(color)}
/>
<AvatarSelector user={$user} onClose={() => (isShowSelectAvatar = false)} onChoose={handleSaveProfile} />
{/if}
@@ -1,24 +1,21 @@
<script lang="ts">
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import FullScreenModal from '../full-screen-modal.svelte';
import UserAvatar from '../user-avatar.svelte';
import { t } from 'svelte-i18n';
export let user: UserResponseDto;
export let onClose: () => void;
export let onChoose: (color: UserAvatarColor) => void;
const dispatch = createEventDispatcher<{
close: void;
choose: UserAvatarColor;
}>();
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
</script>
<FullScreenModal title={$t('select_avatar_color')} width="auto" onClose={() => dispatch('close')}>
<FullScreenModal title={$t('select_avatar_color')} width="auto" {onClose}>
<div class="flex items-center justify-center mt-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
{#each colors as color}
<button type="button" on:click={() => dispatch('choose', color)}>
<button type="button" on:click={() => onChoose(color)}>
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
</button>
{/each}
@@ -10,7 +10,6 @@
import { handleLogout } from '$lib/utils/auth';
import { logout } from '@immich/sdk';
import { mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import { AppRoute } from '../../../constants';
@@ -21,13 +20,11 @@
import AccountInfoPanel from './account-info-panel.svelte';
export let showUploadButton = true;
export let onUploadClick: () => void;
let shouldShowAccountInfo = false;
let shouldShowAccountInfoPanel = false;
let innerWidth: number;
const dispatch = createEventDispatcher<{
uploadClicked: void;
}>();
const onLogout = async () => {
const { redirectUri } = await logout();
@@ -67,14 +64,14 @@
<ThemeButton padding="2" />
{#if !$page.url.pathname.includes('/admin') && showUploadButton}
<LinkButton on:click={() => dispatch('uploadClicked')} class="hidden lg:block">
<LinkButton on:click={onUploadClick} class="hidden lg:block">
<div class="flex gap-2">
<Icon path={mdiTrayArrowUp} size="1.5em" />
<span>{$t('upload')}</span>
</div>
</LinkButton>
<CircleIconButton
on:click={() => dispatch('uploadClicked')}
on:click={onUploadClick}
title={$t('upload')}
icon={mdiTrayArrowUp}
class="lg:hidden"
@@ -114,7 +111,7 @@
{/if}
{#if shouldShowAccountInfoPanel}
<AccountInfoPanel on:logout={onLogout} />
<AccountInfoPanel {onLogout} />
{/if}
</div>
</section>
@@ -56,13 +56,14 @@
return;
}
const file = new File([blob], 'profile-picture.png', { type: 'image/png' });
const { profileImagePath } = await createProfileImage({ createProfileImageDto: { file } });
const { profileImagePath, profileChangedAt } = await createProfileImage({ createProfileImageDto: { file } });
notificationController.show({
type: NotificationType.Info,
message: $t('profile_picture_set'),
timeout: 3000,
});
$user.profileImagePath = profileImagePath;
$user.profileChangedAt = profileChangedAt;
} catch (error) {
handleError(error, $t('errors.unable_to_set_profile_picture'));
}
@@ -8,7 +8,7 @@
<script lang="ts">
import { handlePromiseError } from '$lib/utils';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { tweened } from 'svelte/motion';
/**
@@ -26,6 +26,10 @@
export let duration = 5;
export let onDone: () => void;
export let onPlaying: () => void = () => {};
export let onPaused: () => void = () => {};
const onChange = async () => {
progress = setDuration(duration);
await play();
@@ -39,16 +43,10 @@
$: {
if ($progress === 1) {
dispatch('done');
onDone();
}
}
const dispatch = createEventDispatcher<{
done: void;
playing: void;
paused: void;
}>();
onMount(async () => {
if (autoplay) {
await play();
@@ -57,13 +55,13 @@
export const play = async () => {
status = ProgressBarStatus.Playing;
dispatch('playing');
onPlaying();
await progress.set(1);
};
export const pause = async () => {
status = ProgressBarStatus.Paused;
dispatch('paused');
onPaused();
await progress.set($progress);
};
@@ -20,7 +20,7 @@
title={$t('show_supporter_badge')}
subtitle={$t('show_supporter_badge_description')}
bind:checked={$preferences.purchase.showSupportBadge}
on:toggle={({ detail }) => setSupportBadgeVisibility(detail)}
onToggle={setSupportBadgeVisibility}
/>
</div>
@@ -56,7 +56,7 @@
<div class="w-full">
<Combobox
label={$t('make')}
on:select={({ detail }) => (filters.make = detail?.value)}
onSelect={(option) => (filters.make = option?.value)}
options={asComboboxOptions(makes)}
placeholder={$t('search_camera_make')}
selectedOption={asSelectedOption(makeFilter)}
@@ -66,7 +66,7 @@
<div class="w-full">
<Combobox
label={$t('model')}
on:select={({ detail }) => (filters.model = detail?.value)}
onSelect={(option) => (filters.model = option?.value)}
options={asComboboxOptions(models)}
placeholder={$t('search_camera_model')}
selectedOption={asSelectedOption(modelFilter)}
@@ -73,7 +73,7 @@
<div class="w-full">
<Combobox
label={$t('country')}
on:select={({ detail }) => (filters.country = detail?.value)}
onSelect={(option) => (filters.country = option?.value)}
options={asComboboxOptions(countries)}
placeholder={$t('search_country')}
selectedOption={asSelectedOption(filters.country)}
@@ -83,7 +83,7 @@
<div class="w-full">
<Combobox
label={$t('state')}
on:select={({ detail }) => (filters.state = detail?.value)}
onSelect={(option) => (filters.state = option?.value)}
options={asComboboxOptions(states)}
placeholder={$t('search_state')}
selectedOption={asSelectedOption(filters.state)}
@@ -93,7 +93,7 @@
<div class="w-full">
<Combobox
label={$t('city')}
on:select={({ detail }) => (filters.city = detail?.value)}
onSelect={(option) => (filters.city = option?.value)}
options={asComboboxOptions(cities)}
placeholder={$t('search_city')}
selectedOption={asSelectedOption(filters.city)}
@@ -2,6 +2,7 @@
import { slide } from 'svelte/transition';
import { getAccordionState } from './setting-accordion-state.svelte';
import { onDestroy } from 'svelte';
import Icon from '$lib/components/elements/icon.svelte';
const accordionState = getAccordionState();
@@ -10,6 +11,7 @@
export let key: string;
export let isOpen = $accordionState.has(key);
export let autoScrollTo = false;
export let icon = '';
let accordionElement: HTMLDivElement;
@@ -38,7 +40,12 @@
});
</script>
<div class="border-b-[1px] border-gray-200 py-4 dark:border-gray-700" bind:this={accordionElement}>
<div
class="border rounded-2xl my-4 px-6 py-4 transition-all {isOpen
? 'border-immich-primary/40 dark:border-immich-dark-primary/50 shadow-md'
: 'dark:border-gray-800'}"
bind:this={accordionElement}
>
<button
type="button"
aria-expanded={isOpen}
@@ -46,12 +53,17 @@
class="flex w-full place-items-center justify-between text-left"
>
<div>
<h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h2>
<div class="flex gap-2 place-items-center">
{#if icon}
<Icon path={icon} class="text-immich-primary dark:text-immich-dark-primary" size="24" ariaHidden />
{/if}
<h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h2>
</div>
<slot name="subtitle">
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
<p class="text-sm dark:text-immich-dark-fg mt-1">{subtitle}</p>
</slot>
</div>
@@ -32,14 +32,7 @@
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
</div>
<div class="flex items-center">
<Combobox
label={title}
hideLabel={true}
{selectedOption}
{options}
placeholder={comboboxPlaceholder}
on:select={({ detail }) => onSelect(detail)}
/>
<Combobox label={title} hideLabel={true} {selectedOption} {options} placeholder={comboboxPlaceholder} {onSelect} />
<slot />
</div>
</div>
@@ -43,7 +43,7 @@
icon: option.icon,
};
}}
on:select={({ detail }) => onToggle(detail)}
onSelect={onToggle}
/>
</div>
</div>
@@ -1,7 +1,6 @@
<script lang="ts">
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiChevronDown } from '@mdi/js';
@@ -14,15 +13,14 @@
export let isEdited = false;
export let number = false;
export let disabled = false;
const dispatch = createEventDispatcher<{ select: string | number }>();
export let onSelect: (setting: string | number) => void = () => {};
const handleChange = (e: Event) => {
value = (e.target as HTMLInputElement).value;
if (number) {
value = Number.parseInt(value);
}
dispatch('select', value);
onSelect(value);
};
</script>
@@ -1,7 +1,6 @@
<script lang="ts">
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import Slider from '$lib/components/elements/slider.svelte';
import { generateId } from '$lib/utils/generate-id';
import { t } from 'svelte-i18n';
@@ -11,14 +10,12 @@
export let checked = false;
export let disabled = false;
export let isEdited = false;
export let onToggle: (isChecked: boolean) => void = () => {};
let id: string = generateId();
$: sliderId = `${id}-slider`;
$: subtitleId = subtitle ? `${id}-subtitle` : undefined;
const dispatch = createEventDispatcher<{ toggle: boolean }>();
const onToggle = (isChecked: boolean) => dispatch('toggle', isChecked);
</script>
<div class="flex place-items-center justify-between">
@@ -43,11 +40,5 @@
<slot />
</div>
<Slider
id={sliderId}
bind:checked
{disabled}
on:toggle={({ detail }) => onToggle(detail)}
ariaDescribedBy={subtitleId}
/>
<Slider id={sliderId} bind:checked {disabled} {onToggle} ariaDescribedBy={subtitleId} />
</div>
@@ -1,9 +1,8 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import FullScreenModal from './full-screen-modal.svelte';
import { mdiInformationOutline } from '@mdi/js';
import Icon from '../elements/icon.svelte';
import { t } from 'svelte-i18n';
import Icon from '../elements/icon.svelte';
import FullScreenModal from './full-screen-modal.svelte';
interface Shortcuts {
general: ExplainedShortcut[];
@@ -16,6 +15,8 @@
info?: string;
}
export let onClose: () => void;
export let shortcuts: Shortcuts = {
general: [
{ key: ['←', '→'], action: $t('previous_or_next_photo') },
@@ -33,12 +34,9 @@
{ key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') },
],
};
const dispatch = createEventDispatcher<{
close: void;
}>();
</script>
<FullScreenModal title={$t('keyboard_shortcuts')} width="auto" onClose={() => dispatch('close')}>
<FullScreenModal title={$t('keyboard_shortcuts')} width="auto" {onClose}>
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
{#if shortcuts.general.length > 0}
<div class="p-4">
@@ -13,6 +13,7 @@
email: string;
profileImagePath: string;
avatarColor: UserAvatarColor;
profileChangedAt: string;
}
export let user: User;
@@ -79,7 +80,7 @@
{#if showProfileImage && user.profileImagePath}
<img
bind:this={img}
src={getProfileImageUrl(user.id)}
src={getProfileImageUrl(user)}
alt={$t('profile_image_of_user', { values: { user: title } })}
class="h-full w-full object-cover"
class:hidden={showFallback}
@@ -99,7 +99,7 @@
title={$t('theme_selection')}
subtitle={$t('theme_selection_description')}
bind:checked={$colorTheme.system}
on:toggle={handleToggleColorTheme}
onToggle={handleToggleColorTheme}
/>
</div>
@@ -119,7 +119,7 @@
title={$t('default_locale')}
subtitle={$t('default_locale_description')}
checked={$locale == undefined}
on:toggle={handleToggleLocaleBrowser}
onToggle={handleToggleLocaleBrowser}
>
<p class="mt-2 dark:text-gray-400">{selectedDate}</p>
</SettingSwitch>
@@ -142,7 +142,7 @@
title={$t('display_original_photos')}
subtitle={$t('display_original_photos_setting_description')}
bind:checked={$alwaysLoadOriginalFile}
on:toggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
onToggle={() => ($alwaysLoadOriginalFile = !$alwaysLoadOriginalFile)}
/>
</div>
<div class="ml-4">
@@ -150,7 +150,7 @@
title={$t('video_hover_setting')}
subtitle={$t('video_hover_setting_description')}
bind:checked={$playVideoThumbnailOnHover}
on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
onToggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
/>
</div>
<div class="ml-4">
@@ -158,7 +158,7 @@
title={$t('loop_videos')}
subtitle={$t('loop_videos_description')}
bind:checked={$loopVideo}
on:toggle={() => ($loopVideo = !$loopVideo)}
onToggle={() => ($loopVideo = !$loopVideo)}
/>
</div>
@@ -15,14 +15,10 @@
mdiUbuntu,
} from '@mdi/js';
import { DateTime, type ToRelativeCalendarOptions } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
export let device: SessionResponseDto;
const dispatcher = createEventDispatcher<{
delete: void;
}>();
export let onDelete: (() => void) | undefined = undefined;
const options: ToRelativeCalendarOptions = {
unit: 'days',
@@ -68,14 +64,14 @@
</span>
</div>
</div>
{#if !device.current}
{#if !device.current && onDelete}
<div>
<CircleIconButton
color="primary"
icon={mdiTrashCanOutline}
title={$t('log_out')}
size="16"
on:click={() => dispatcher('delete')}
on:click={onDelete}
/>
</div>
{/if}
@@ -68,7 +68,7 @@
{$t('other_devices').toUpperCase()}
</h3>
{#each otherDevices as device, index}
<DeviceCard {device} on:delete={() => handleDelete(device)} />
<DeviceCard {device} onDelete={() => handleDelete(device)} />
{#if index !== otherDevices.length - 1}
<hr class="my-3" />
{/if}
@@ -55,7 +55,7 @@
<section class="my-4">
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4">
<div class="ml-4 mt-4 flex flex-col">
<SettingAccordion key="folders" title={$t('folders')} subtitle={$t('folders_feature_description')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={foldersEnabled} />
@@ -1,19 +1,18 @@
<script lang="ts">
import { searchUsers, getPartners, type UserResponseDto, PartnerDirection } from '@immich/sdk';
import { createEventDispatcher, onMount } from 'svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { getPartners, PartnerDirection, searchUsers, type UserResponseDto } from '@immich/sdk';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import Button from '../elements/buttons/button.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { t } from 'svelte-i18n';
export let user: UserResponseDto;
export let onClose: () => void;
export let onAddUsers: (users: UserResponseDto[]) => void;
let availableUsers: UserResponseDto[] = [];
let selectedUsers: UserResponseDto[] = [];
const dispatch = createEventDispatcher<{ 'add-users': UserResponseDto[] }>();
onMount(async () => {
let users = await searchUsers();
@@ -69,7 +68,7 @@
{#if selectedUsers.length > 0}
<div class="pt-5">
<Button size="sm" fullwidth on:click={() => dispatch('add-users', selectedUsers)}>{$t('add')}</Button>
<Button size="sm" fullwidth on:click={() => onAddUsers(selectedUsers)}>{$t('add')}</Button>
</div>
{/if}
</div>
@@ -177,7 +177,7 @@
title={$t('show_in_timeline')}
subtitle={$t('show_in_timeline_setting_description')}
bind:checked={partner.inTimeline}
on:toggle={({ detail }) => handleShowOnTimelineChanged(partner, detail)}
onToggle={(isChecked) => handleShowOnTimelineChanged(partner, isChecked)}
/>
{/if}
</div>
@@ -191,9 +191,5 @@
</section>
{#if createPartnerFlag}
<PartnerSelectionModal
{user}
onClose={() => (createPartnerFlag = false)}
on:add-users={(event) => handleCreatePartners(event.detail)}
/>
<PartnerSelectionModal {user} onClose={() => (createPartnerFlag = false)} onAddUsers={handleCreatePartners} />
{/if}
@@ -102,7 +102,7 @@
{/if}
{#if secret}
<APIKeySecret {secret} on:done={() => (secret = '')} />
<APIKeySecret {secret} onDone={() => (secret = '')} />
{/if}
{#if editKey}
@@ -115,7 +115,7 @@
title={$t('show_supporter_badge')}
subtitle={$t('show_supporter_badge_description')}
bind:checked={$preferences.purchase.showSupportBadge}
on:toggle={({ detail }) => setSupportBadgeVisibility(detail)}
onToggle={setSupportBadgeVisibility}
/>
</div>
@@ -19,6 +19,19 @@
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte';
import {
mdiAccountGroupOutline,
mdiAccountOutline,
mdiApi,
mdiBellOutline,
mdiCogOutline,
mdiDevices,
mdiDownload,
mdiFeatureSearchOutline,
mdiKeyOutline,
mdiOnepassword,
mdiTwoFactorAuthentication,
} from '@mdi/js';
export let keys: ApiKeyResponseDto[] = [];
export let sessions: SessionResponseDto[] = [];
@@ -29,23 +42,34 @@
</script>
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
<SettingAccordion key="app-settings" title={$t('app_settings')} subtitle={$t('manage_the_app_settings')}>
<SettingAccordion
icon={mdiCogOutline}
key="app-settings"
title={$t('app_settings')}
subtitle={$t('manage_the_app_settings')}
>
<AppSettings />
</SettingAccordion>
<SettingAccordion key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
<SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
<UserProfileSettings />
</SettingAccordion>
<SettingAccordion key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
<SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
<UserAPIKeyList bind:keys />
</SettingAccordion>
<SettingAccordion key="authorized-devices" title={$t('authorized_devices')} subtitle={$t('manage_your_devices')}>
<SettingAccordion
icon={mdiDevices}
key="authorized-devices"
title={$t('authorized_devices')}
subtitle={$t('manage_your_devices')}
>
<DeviceList bind:devices={sessions} />
</SettingAccordion>
<SettingAccordion
icon={mdiDownload}
key="download-settings"
title={$t('download_settings')}
subtitle={$t('download_settings_description')}
@@ -53,16 +77,27 @@
<DownloadSettings />
</SettingAccordion>
<SettingAccordion key="feature" title={$t('features')} subtitle={$t('features_setting_description')}>
<SettingAccordion
icon={mdiFeatureSearchOutline}
key="feature"
title={$t('features')}
subtitle={$t('features_setting_description')}
>
<FeatureSettings />
</SettingAccordion>
<SettingAccordion key="notifications" title={$t('notifications')} subtitle={$t('notifications_setting_description')}>
<SettingAccordion
icon={mdiBellOutline}
key="notifications"
title={$t('notifications')}
subtitle={$t('notifications_setting_description')}
>
<NotificationsSettings />
</SettingAccordion>
{#if $featureFlags.loaded && $featureFlags.oauth}
<SettingAccordion
icon={mdiTwoFactorAuthentication}
key="oauth"
title={$t('oauth')}
subtitle={$t('manage_your_oauth_connection')}
@@ -72,15 +107,21 @@
</SettingAccordion>
{/if}
<SettingAccordion key="password" title={$t('password')} subtitle={$t('change_your_password')}>
<SettingAccordion icon={mdiOnepassword} key="password" title={$t('password')} subtitle={$t('change_your_password')}>
<ChangePasswordSettings />
</SettingAccordion>
<SettingAccordion key="partner-sharing" title={$t('partner_sharing')} subtitle={$t('manage_sharing_with_partners')}>
<SettingAccordion
icon={mdiAccountGroupOutline}
key="partner-sharing"
title={$t('partner_sharing')}
subtitle={$t('manage_sharing_with_partners')}
>
<PartnerSettings user={$user} />
</SettingAccordion>
<SettingAccordion
icon={mdiKeyOutline}
key="user-purchase-settings"
title={$t('user_purchase_settings')}
subtitle={$t('user_purchase_settings_description')}
@@ -151,15 +151,15 @@
<AssetViewer
asset={$viewingAsset}
showNavigation={assets.length > 1}
on:next={() => {
onNext={() => {
const index = getAssetIndex($viewingAsset.id) + 1;
setAsset(assets[index % assets.length]);
}}
on:previous={() => {
onPrevious={() => {
const index = getAssetIndex($viewingAsset.id) - 1 + assets.length;
setAsset(assets[index % assets.length]);
}}
on:close={() => {
onClose={() => {
assetViewingStore.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}}
+1 -1
View File
@@ -148,7 +148,7 @@
"note_cannot_be_changed_later": "ملاحظة: لا يمكن تغيير هذا لاحقًا!",
"note_unlimited_quota": "ملاحظة: أدخل 0 للحصول على حصة غير محدودة",
"notification_email_from_address": "عنوان المرسل",
"notification_email_from_address_description": "عنوان البريد الإلكتروني للمرسل، على سبيل المثال: \"Immich Photo Server noreply@immich.app\"",
"notification_email_from_address_description": "عنوان البريد الإلكتروني للمرسل، على سبيل المثال: \"Immich Photo Server noreply@example.com\"",
"notification_email_host_description": "مضيف خادم البريد الإلكتروني (مثلًا: smtp.immich.app)",
"notification_email_ignore_certificate_errors": "تجاهل أخطاء الشهادة",
"notification_email_ignore_certificate_errors_description": "تجاهل أخطاء التحقق من صحة شهادة TLS (غير مستحسن)",
+53 -29
View File
@@ -137,7 +137,7 @@
"map_settings_description": "Управление на настройките на картата",
"map_style_description": "URL адрес към файл \"style.json\" за задаване на стил на картата",
"metadata_extraction_job": "Извличане на метаданни",
"metadata_extraction_job_description": "Извличане на метаданни от всеки ресурс, като GPS и резолюция",
"metadata_extraction_job_description": "Извличане на метаданни от всеки от ресурсите, като GPS локация, лица и резолюция на файловете",
"metadata_faces_import_setting": "Включи импорт на лице",
"metadata_faces_import_setting_description": "Импортирай лица от EXIF данни и помощни файлове",
"metadata_settings": "Опции за метаданни",
@@ -150,7 +150,7 @@
"note_cannot_be_changed_later": "ВНИМАНИЕ: Това не може да бъде променено по-късно!",
"note_unlimited_quota": "Бележка: Въведете 0 за да нямате лимит на квотата",
"notification_email_from_address": "От адрес",
"notification_email_from_address_description": "Електронна поща на изпращача, например: \"Immich Photo Server <noreply@immich.app>\"",
"notification_email_from_address_description": "Електронна поща на изпращача, например: \"Immich Photo Server <noreply@example.com>\"",
"notification_email_host_description": "Хост на сървъра за електронна поща (например: smtp.immich.app)",
"notification_email_ignore_certificate_errors": "Игорнорирайте сертификационни грешки",
"notification_email_ignore_certificate_errors_description": "Игнорирай грешки свързани с валидация на TLS сертификат (не се препоръчва)",
@@ -176,7 +176,7 @@
"oauth_issuer_url": "URL на издателя",
"oauth_mobile_redirect_uri": "URI за мобилно пренасочване",
"oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства",
"oauth_mobile_redirect_uri_override_description": "Разреши когато 'app.immich:/' е невалиден пренасочвар адрес/URI.",
"oauth_mobile_redirect_uri_override_description": "Разреши когато доставчика за OAuth удостоверяване не позволява за мобилни URI идентификатори, като '{callback}'",
"oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили",
"oauth_profile_signing_algorithm_description": "Алгоритъм излпозлван за вписване на потребителски профил.",
"oauth_scope": "Област/обхват на приложение",
@@ -244,7 +244,7 @@
"thumbnail_generation_job": "Генериране на миниатюри",
"thumbnail_generation_job_description": "Генерирайте големи, малки и замъглени миниатюри за всеки актив, както и миниатюри за всеки човек",
"transcoding_acceleration_api": "API за ускоряване",
"transcoding_acceleration_api_description": "API, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „best effort“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може или не може да работи в зависимост от вашия хардуер.",
"transcoding_acceleration_api_description": "API интерфейсът, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „възможно най-доброто“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може и да не работи в зависимост от вашия хардуер.",
"transcoding_acceleration_nvenc": "NVENC (необходим NVIDIA GPU)",
"transcoding_acceleration_qsv": "Quick Sync (необходим 7th поколение Intel CPU или по-ново)",
"transcoding_acceleration_rkmpp": "RKMPP (само на Rockchip SOCs)",
@@ -252,9 +252,9 @@
"transcoding_accepted_audio_codecs": "Допустими аудио кодеци",
"transcoding_accepted_audio_codecs_description": "Изберете кои аудио кодеци не са нужни за разкодиране. Използва се само за определени правила за разкодиране.",
"transcoding_accepted_containers": "Приети контейнери",
"transcoding_accepted_containers_description": "Изберете кои формати на контейнери не трябва да се пренасочват към MP4. Използва се само за определени правила за разкодиране.",
"transcoding_accepted_containers_description": "Изберете кои формати на контейнери не е нужно да бъдат преобразувани в MP4 формат. Използва се само за определени правила за разкодиране.",
"transcoding_accepted_video_codecs": "Приети видео кодеци",
"transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябва да се разкодиране. Използва се само за определени правила за разкодиране.",
"transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябват за разкодиране. Използва се само за определени правила за разкодиране.",
"transcoding_advanced_options_description": "Опции, които повечето потребители не трябва да променят",
"transcoding_audio_codec": "Аудио кодек",
"transcoding_audio_codec_description": "Opus е опцията с най-високо качество, но има по-ниска съвместимост със стари устройства или софтуер.",
@@ -446,7 +446,7 @@
"copy_to_clipboard": "Копиране в клипборда",
"country": "Държава",
"cover": "",
"covers": "",
"covers": "Обложка",
"create": "Създай",
"create_album": "Създай албум",
"create_library": "Създай библиотека",
@@ -938,19 +938,22 @@
"search_city": "",
"search_country": "",
"search_for_existing_person": "",
"search_people": "",
"search_places": "",
"search_people": "Търсете на хора",
"search_places": "Търсене на места",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_your_photos": "",
"search_tags": "Търсене на етикети...",
"search_timezone": "Търсене на часова зона...",
"search_type": "Тип на търсене",
"search_your_photos": "Търсете вашите снимки",
"searching_locales": "",
"second": "Секунда",
"select_album_cover": "",
"select_all": "",
"select_avatar_color": "",
"select_face": "",
"see_all_people": "Вижте всички хора",
"select_album_cover": "Изберете обложка на албум",
"select_all": "Изберете всички",
"select_avatar_color": "Изберете цвят на аватара",
"select_face": "Изберете лице",
"select_featured_photo": "",
"select_from_computer": "Изберете от компютъра",
"select_keep_all": "",
"select_library_owner": "Изберете собственик на библиотека",
"select_new_face": "Изберете ново лице",
@@ -998,28 +1001,40 @@
"show_metadata": "Покажи метаданни",
"show_or_hide_info": "Покажи или скрий информацията",
"show_password": "Покажи паролата",
"show_person_options": "",
"show_progress_bar": "",
"show_search_options": "",
"show_person_options": "Показване на опции за лица",
"show_progress_bar": "Показване на прогрес бара",
"show_search_options": "Показване на опциите за търсене",
"show_supporter_badge": "Значка поддръжник",
"show_supporter_badge_description": "Покажи значка поддръжник",
"shuffle": "Разбъркване",
"sign_out": "",
"sign_up": "",
"sidebar": "Странична лента",
"sidebar_display_description": "Показване на връзка към изгледа в страничната лента",
"sign_out": "Отписване",
"sign_up": "Запиши се",
"size": "Размер",
"skip_to_content": "",
"skip_to_content": "Премини към съдържанието",
"skip_to_folders": "Премини към папките",
"skip_to_tags": "Премини към етикетите",
"slideshow": "Слайдшоу",
"slideshow_settings": "",
"sort_albums_by": "",
"slideshow_settings": "Настройки за слайдшоу",
"sort_albums_by": "Сортиране на албуми по...",
"sort_created": "Дата на създаване",
"sort_items": "Брой елементи",
"sort_modified": "Дата на промяна",
"sort_oldest": "Най-старата снимка",
"sort_recent": "Най-новата снимка",
"sort_title": "Заглавие",
"source": "Източник",
"stack": "",
"stack_selected_photos": "",
"stack_duplicates": "Подреждане на дубликати",
"stack_selected_photos": "Подреждане на избрани снимки",
"stacktrace": "",
"start": "Старт",
"start_date": "",
"start_date": "Начална дата",
"state": "",
"status": "Статус",
"stop_motion_photo": "",
"stop_photo_sharing": "Да спрете ли споделянето на вашите снимки?",
"stop_photo_sharing": "Да спра ли споделянето на вашите снимки?",
"stop_photo_sharing_description": "{partner} вече няма достъп до вашите снимки.",
"stop_sharing_photos_with_user": "Прекратете споделянето на снимки с този потребител",
"storage": "Пространство на хранилището",
@@ -1030,6 +1045,12 @@
"sunrise_on_the_beach": "Изгрев на плажа",
"swap_merge_direction": "Размяна посоката на сливане",
"sync": "Синхронизиране",
"tag": "Таг",
"tag_created": "Създаден етикет: {tag}",
"tag_feature_description": "Разглеждане на снимки и видеоклипове, групирани по теми с логически тагове",
"tag_not_found_question": "Не можете да намерите етикет? Създайте такъв <link>тук</link>",
"tag_updated": "Актуализиран етикет: {tag}",
"tags": "Етикет",
"template": "Шаблон",
"theme": "Тема",
"theme_selection": "Избор на тема",
@@ -1048,7 +1069,7 @@
"total_usage": "Общо използвано",
"trash": "кошче",
"trash_all": "Изхвърли всички",
"trash_count": "",
"trash_count": "Кошче {count, number}",
"trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.",
"trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# day} other {# days}}.",
"type": "Тип",
@@ -1062,6 +1083,7 @@
"unlink_oauth": "",
"unlinked_oauth_account": "",
"unnamed_album": "Албум без име",
"unnamed_album_delete_confirmation": "Сигурни ли сте, че искате да изтриете този албум?",
"unnamed_share": "Споделяне без име",
"unsaved_change": "Незапазена промяна",
"unselect_all": "Деселектирайте всички",
@@ -1085,7 +1107,7 @@
"user_purchase_settings": "Покупка",
"user_purchase_settings_description": "Управлявай покупката си",
"user_role_set": "Задай {user} като {role}",
"user_usage_detail": "",
"user_usage_detail": "Подробности за използването на потребителя",
"username": "Потребителско име",
"users": "Потребители",
"utilities": "Инструменти",
@@ -1103,9 +1125,11 @@
"view_album": "Разгледай албума",
"view_all": "Преглед на всички",
"view_all_users": "Преглед на всички потребители",
"view_in_timeline": "Покажи във времева линия",
"view_links": "Преглед на връзките",
"view_next_asset": "Преглед на следващия файл",
"view_previous_asset": "Преглед на предишния файл",
"view_stack": "Покажи в стек",
"viewer": "",
"waiting": "в изчакване",
"warning": "Внимание",

Some files were not shown because too many files have changed in this diff Show More