mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
refactor: trash page actions (#25039)
This commit is contained in:
parent
3762728c84
commit
519a7df4cd
3 changed files with 94 additions and 65 deletions
|
|
@ -6,8 +6,11 @@
|
||||||
import { useActions, type ActionArray } from '$lib/actions/use-actions';
|
import { useActions, type ActionArray } from '$lib/actions/use-actions';
|
||||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||||
import UserSidebar from '$lib/components/shared-components/side-bar/user-sidebar.svelte';
|
import UserSidebar from '$lib/components/shared-components/side-bar/user-sidebar.svelte';
|
||||||
|
import type { HeaderButtonActionItem } from '$lib/types';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
|
import { Button, ContextMenuButton, HStack, isMenuItemType, type MenuItemType } from '@immich/ui';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hideNavbar?: boolean;
|
hideNavbar?: boolean;
|
||||||
|
|
@ -16,6 +19,7 @@
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
scrollbar?: boolean;
|
scrollbar?: boolean;
|
||||||
use?: ActionArray;
|
use?: ActionArray;
|
||||||
|
actions?: Array<HeaderButtonActionItem | MenuItemType>;
|
||||||
header?: Snippet;
|
header?: Snippet;
|
||||||
sidebar?: Snippet;
|
sidebar?: Snippet;
|
||||||
buttons?: Snippet;
|
buttons?: Snippet;
|
||||||
|
|
@ -29,6 +33,7 @@
|
||||||
description = undefined,
|
description = undefined,
|
||||||
scrollbar = true,
|
scrollbar = true,
|
||||||
use = [],
|
use = [],
|
||||||
|
actions = [],
|
||||||
header,
|
header,
|
||||||
sidebar,
|
sidebar,
|
||||||
buttons,
|
buttons,
|
||||||
|
|
@ -74,7 +79,31 @@
|
||||||
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
|
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{@render buttons?.()}
|
{@render buttons?.()}
|
||||||
|
|
||||||
|
{#if actions.length > 0}
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<HStack gap={0}>
|
||||||
|
{#each actions as action, i (i)}
|
||||||
|
{#if !isMenuItemType(action) && (action.$if?.() ?? true)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
color={action.color ?? 'secondary'}
|
||||||
|
leadingIcon={action.icon}
|
||||||
|
onclick={() => action.onAction(action)}
|
||||||
|
title={action.data?.title}
|
||||||
|
>
|
||||||
|
{action.title}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
54
web/src/lib/services/trash.service.ts
Normal file
54
web/src/lib/services/trash.service.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { emptyTrash, restoreTrash } from '@immich/sdk';
|
||||||
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
|
import { mdiDeleteForeverOutline, mdiHistory } from '@mdi/js';
|
||||||
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export const getTrashActions = ($t: MessageFormatter) => {
|
||||||
|
const RestoreAll: ActionItem = {
|
||||||
|
title: $t('restore_all'),
|
||||||
|
icon: mdiHistory,
|
||||||
|
onAction: () => handleRestoreTrash(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Empty: ActionItem = {
|
||||||
|
title: $t('empty_trash'),
|
||||||
|
icon: mdiDeleteForeverOutline,
|
||||||
|
onAction: () => handleEmptyTrash(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { RestoreAll, Empty };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleEmptyTrash = async () => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
const confirmed = await modalManager.showDialog({ prompt: $t('empty_trash_confirmation') });
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { count } = await emptyTrash();
|
||||||
|
toastManager.success($t('assets_permanently_deleted_count', { values: { count } }));
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_empty_trash'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleRestoreTrash = async () => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
const confirmed = await modalManager.showDialog({ prompt: $t('assets_restore_confirmation') });
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { count } = await restoreTrash();
|
||||||
|
toastManager.success($t('assets_restored_count', { values: { count } }));
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_restore_trash'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -12,18 +12,15 @@
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
|
import { getTrashActions } from '$lib/services/trash.service';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import { emptyTrash, restoreTrash } from '@immich/sdk';
|
|
||||||
import { Button, HStack, modalManager, Text, toastManager } from '@immich/ui';
|
|
||||||
import { mdiDeleteForeverOutline, mdiHistory } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
}
|
};
|
||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
|
@ -36,74 +33,23 @@
|
||||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEmptyTrash = async () => {
|
|
||||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('empty_trash_confirmation') });
|
|
||||||
if (!isConfirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { count } = await emptyTrash();
|
|
||||||
toastManager.success($t('assets_permanently_deleted_count', { values: { count } }));
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_empty_trash'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRestoreTrash = async () => {
|
|
||||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('assets_restore_confirmation') });
|
|
||||||
if (!isConfirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { count } = await restoreTrash();
|
|
||||||
toastManager.success($t('assets_restored_count', { values: { count } }));
|
|
||||||
|
|
||||||
// reset asset grid (TODO fix in asset store that it should reset when it is empty)
|
|
||||||
// note - this is still a problem, but updateOptions with the same value will not
|
|
||||||
// do anything, so need to flip it for it to reload/reinit
|
|
||||||
// await timelineManager.updateOptions({ deferInit: true, isTrashed: true });
|
|
||||||
// await timelineManager.updateOptions({ deferInit: false, isTrashed: true });
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_restore_trash'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { Empty, RestoreAll } = $derived(getTrashActions($t));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if featureFlagsManager.value.trash}
|
{#if featureFlagsManager.value.trash}
|
||||||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
<UserPageLayout
|
||||||
{#snippet buttons()}
|
hideNavbar={assetInteraction.selectionActive}
|
||||||
<HStack gap={0}>
|
actions={assetInteraction.selectionActive ? [] : [Empty, RestoreAll]}
|
||||||
<Button
|
title={data.meta.title}
|
||||||
leadingIcon={mdiHistory}
|
scrollbar={false}
|
||||||
onclick={handleRestoreTrash}
|
>
|
||||||
disabled={assetInteraction.selectionActive}
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<Text class="hidden md:block">{$t('restore_all')}</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leadingIcon={mdiDeleteForeverOutline}
|
|
||||||
onclick={() => handleEmptyTrash()}
|
|
||||||
disabled={assetInteraction.selectionActive}
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<Text class="hidden md:block">{$t('empty_trash')}</Text>
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<Timeline enableRouting={true} bind:timelineManager {options} {assetInteraction} onEscape={handleEscape}>
|
<Timeline enableRouting={true} bind:timelineManager {options} {assetInteraction} onEscape={handleEscape}>
|
||||||
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4">
|
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4">
|
||||||
{$t('trashed_items_will_be_permanently_deleted_after', {
|
{$t('trashed_items_will_be_permanently_deleted_after', {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue