feat: preload textual model
This commit is contained in:
+1
-1
@@ -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).
|
||||
|
||||
Generated
+276
-539
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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
@@ -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}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -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(() => {
|
||||
|
||||
+2
-6
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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 }));
|
||||
}}
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user