mirror of
https://github.com/immich-app/immich.git
synced 2026-06-22 14:52:17 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 881bd83ccf |
@@ -20,7 +20,6 @@
|
||||
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { getSharedLink, handlePromiseError } from '$lib/utils';
|
||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { navigateToAsset } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
||||
@@ -69,7 +68,6 @@
|
||||
onAssetChange?: (asset: AssetResponseDto) => void;
|
||||
preAction?: PreAction;
|
||||
onAction?: OnAction;
|
||||
onUndoDelete?: OnUndoDelete;
|
||||
onClose?: (assetId: string) => void;
|
||||
onRemoveFromAlbum?: (assetIds: string[]) => void;
|
||||
onRandom?: () => Promise<{ id: string } | undefined>;
|
||||
@@ -85,7 +83,6 @@
|
||||
onAssetChange,
|
||||
preAction,
|
||||
onAction,
|
||||
onUndoDelete,
|
||||
onClose,
|
||||
onRemoveFromAlbum,
|
||||
onRandom,
|
||||
@@ -314,11 +311,6 @@
|
||||
|
||||
const handleAction = async (action: Action) => {
|
||||
switch (action.type) {
|
||||
case AssetAction.DELETE:
|
||||
case AssetAction.TRASH: {
|
||||
eventManager.emit('AssetsDelete', [asset.id]);
|
||||
break;
|
||||
}
|
||||
case AssetAction.REMOVE_ASSET_FROM_STACK: {
|
||||
stack = action.stack;
|
||||
if (stack) {
|
||||
@@ -501,7 +493,6 @@
|
||||
{stack}
|
||||
preAction={handlePreAction}
|
||||
onAction={handleAction}
|
||||
{onUndoDelete}
|
||||
onClose={onClose ? () => onClose(stack?.primaryAssetId ?? asset.id) : undefined}
|
||||
{onRemoveFromAlbum}
|
||||
{playOriginalVideo}
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action';
|
||||
import AddToStackAction from '$lib/components/asset-viewer/actions/AddToStackAction.svelte';
|
||||
import ArchiveAction from '$lib/components/asset-viewer/actions/ArchiveAction.svelte';
|
||||
import DeleteAction from '$lib/components/asset-viewer/actions/DeleteAction.svelte';
|
||||
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/KeepThisDeleteOthers.svelte';
|
||||
import RatingAction from '$lib/components/asset-viewer/actions/RatingAction.svelte';
|
||||
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/RemoveAssetFromStack.svelte';
|
||||
import RestoreAction from '$lib/components/asset-viewer/actions/RestoreAction.svelte';
|
||||
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/SetPersonFeaturedAction.svelte';
|
||||
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/SetProfilePictureAction.svelte';
|
||||
import SetStackPrimaryAsset from '$lib/components/asset-viewer/actions/SetStackPrimaryAsset.svelte';
|
||||
@@ -25,9 +23,8 @@
|
||||
import { Route } from '$lib/route';
|
||||
import { getAlbumAssetActions } from '$lib/services/album.service';
|
||||
import { getGlobalActions } from '$lib/services/app.service';
|
||||
import { getAssetActions } from '$lib/services/asset.service';
|
||||
import { getAssetActions, handleTrashOrDelete } from '$lib/services/asset.service';
|
||||
import { getSharedLink, withoutIcons } from '$lib/utils';
|
||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import {
|
||||
AssetTypeEnum,
|
||||
@@ -37,7 +34,7 @@
|
||||
type PersonResponseDto,
|
||||
type StackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { ActionButton, CommandPaletteDefaultProvider, Tooltip, type ActionItem } from '@immich/ui';
|
||||
import { ActionButton, CommandPaletteDefaultProvider, shortcut, Tooltip, type ActionItem } from '@immich/ui';
|
||||
import { mdiArrowLeft, mdiArrowRight, mdiCompare, mdiDotsVertical, mdiImageSearch, mdiVideoOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -48,7 +45,6 @@
|
||||
stack?: StackResponseDto | null;
|
||||
preAction: PreAction;
|
||||
onAction: OnAction;
|
||||
onUndoDelete?: OnUndoDelete;
|
||||
onClose?: () => void;
|
||||
onRemoveFromAlbum?: (assetIds: string[]) => void;
|
||||
playOriginalVideo: boolean;
|
||||
@@ -62,7 +58,6 @@
|
||||
stack = null,
|
||||
preAction,
|
||||
onAction,
|
||||
onUndoDelete = undefined,
|
||||
onClose,
|
||||
onRemoveFromAlbum,
|
||||
playOriginalVideo = false,
|
||||
@@ -88,6 +83,10 @@
|
||||
const sharedLink = getSharedLink();
|
||||
</script>
|
||||
|
||||
<svelte:document
|
||||
use:shortcut={{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => handleTrashOrDelete(asset, true) }}
|
||||
/>
|
||||
|
||||
<CommandPaletteDefaultProvider name={$t('assets')} actions={withoutIcons([Close, Cast, ...Object.values(Actions)])} />
|
||||
|
||||
<div
|
||||
@@ -128,10 +127,8 @@
|
||||
{/if}
|
||||
|
||||
<ActionButton action={Actions.Edit} />
|
||||
|
||||
{#if isOwner}
|
||||
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
||||
{/if}
|
||||
<ActionButton action={Actions.Delete} />
|
||||
<ActionButton action={Actions.PermanentlyDelete} />
|
||||
|
||||
{#if !sharedLink}
|
||||
<ButtonContextMenu direction="left" align="top-right" color="secondary" title={$t('more')} icon={mdiDotsVertical}>
|
||||
@@ -139,10 +136,7 @@
|
||||
|
||||
<ActionMenuItem action={Actions.Download} />
|
||||
<ActionMenuItem action={Actions.DownloadOriginal} />
|
||||
|
||||
{#if !isLocked && asset.isTrashed}
|
||||
<RestoreAction {asset} {onAction} />
|
||||
{/if}
|
||||
<ActionMenuItem action={Actions.Restore} />
|
||||
|
||||
<ActionMenuItem action={Actions.AddToAlbum} />
|
||||
{#if album && (isOwner || isAlbumOwner)}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import '@testing-library/jest-dom';
|
||||
import { renderWithTooltips } from '$tests/helpers';
|
||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||
import DeleteAction from './DeleteAction.svelte';
|
||||
|
||||
let asset: AssetResponseDto;
|
||||
|
||||
describe('DeleteAction component', () => {
|
||||
beforeEach(() => {
|
||||
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: { trash: true } } as any };
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an asset which is not trashed yet', () => {
|
||||
beforeEach(() => {
|
||||
asset = assetFactory.build({ isTrashed: false });
|
||||
});
|
||||
|
||||
it('displays a button to move the asset to the trash bin', () => {
|
||||
const { getByLabelText, queryByTitle } = renderWithTooltips(DeleteAction, {
|
||||
asset,
|
||||
onAction: vi.fn(),
|
||||
preAction: vi.fn(),
|
||||
});
|
||||
expect(getByLabelText('delete')).toBeInTheDocument();
|
||||
expect(queryByTitle('deletePermanently')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('but if the asset is already trashed', () => {
|
||||
beforeEach(() => {
|
||||
asset = assetFactory.build({ isTrashed: true });
|
||||
});
|
||||
|
||||
it('displays a button to permanently delete the asset', () => {
|
||||
const { getByLabelText, queryByTitle } = renderWithTooltips(DeleteAction, {
|
||||
asset,
|
||||
onAction: vi.fn(),
|
||||
preAction: vi.fn(),
|
||||
});
|
||||
expect(getByLabelText('permanently_delete')).toBeInTheDocument();
|
||||
expect(queryByTitle('delete')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
import { deleteAssets as deleteAssetsUtil, type OnUndoDelete } from '$lib/utils/actions';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
|
||||
import { IconButton, modalManager, toastManager } from '@immich/ui';
|
||||
import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { OnAction, PreAction } from './action';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
onAction: OnAction;
|
||||
preAction: PreAction;
|
||||
onUndoDelete?: OnUndoDelete;
|
||||
}
|
||||
|
||||
let { asset, onAction, preAction, onUndoDelete = undefined }: Props = $props();
|
||||
|
||||
const forceDefault = $derived(asset.isTrashed || !featureFlagsManager.value.trash);
|
||||
|
||||
const trashOrDelete = async (forceRequest?: boolean) => {
|
||||
const timelineAsset = toTimelineAsset(asset);
|
||||
const force = forceDefault || forceRequest;
|
||||
|
||||
if (force) {
|
||||
if ($showDeleteModal) {
|
||||
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: 1 });
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
preAction({ type: AssetAction.DELETE, asset: timelineAsset });
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } });
|
||||
onAction({ type: AssetAction.DELETE, asset: timelineAsset });
|
||||
toastManager.primary($t('permanently_deleted_asset'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_asset'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
preAction({ type: AssetAction.TRASH, asset: timelineAsset });
|
||||
await deleteAssetsUtil(
|
||||
false,
|
||||
() => onAction({ type: AssetAction.TRASH, asset: timelineAsset }),
|
||||
[timelineAsset],
|
||||
onUndoDelete,
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:document
|
||||
use:shortcuts={[
|
||||
{ shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete() },
|
||||
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
|
||||
]}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
color="secondary"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
icon={forceDefault ? mdiDeleteForeverOutline : mdiDeleteOutline}
|
||||
aria-label={forceDefault ? $t('permanently_delete') : $t('delete')}
|
||||
onclick={() => trashOrDelete()}
|
||||
/>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { restoreAssets, type AssetResponseDto } from '@immich/sdk';
|
||||
import { toastManager } from '@immich/ui';
|
||||
import { mdiHistory } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { OnAction } from './action';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
onAction: OnAction;
|
||||
}
|
||||
|
||||
let { asset = $bindable(), onAction }: Props = $props();
|
||||
|
||||
const handleRestoreAsset = async () => {
|
||||
try {
|
||||
await restoreAssets({ bulkIdsDto: { ids: [asset.id] } });
|
||||
asset.isTrashed = false;
|
||||
onAction({ type: AssetAction.RESTORE, asset: toTimelineAsset(asset) });
|
||||
toastManager.primary($t('restored_asset'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_restore_assets'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<MenuOption icon={mdiHistory} onClick={handleRestoreAsset} text={$t('restore')} />
|
||||
@@ -5,9 +5,6 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
type ActionMap = {
|
||||
[AssetAction.ARCHIVE]: { asset: TimelineAsset };
|
||||
[AssetAction.UNARCHIVE]: { asset: TimelineAsset };
|
||||
[AssetAction.TRASH]: { asset: TimelineAsset };
|
||||
[AssetAction.DELETE]: { asset: TimelineAsset };
|
||||
[AssetAction.RESTORE]: { asset: TimelineAsset };
|
||||
[AssetAction.STACK]: { stack: StackResponseDto };
|
||||
[AssetAction.UNSTACK]: { assets: TimelineAsset[] };
|
||||
[AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto };
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import ControlAppBar from '../shared-components/ControlAppBar.svelte';
|
||||
import GalleryViewer from '../shared-components/gallery-viewer/GalleryViewer.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
|
||||
interface Props {
|
||||
sharedLink: SharedLinkResponseDto;
|
||||
@@ -63,15 +64,20 @@
|
||||
|
||||
const handleAction = async (action: Action) => {
|
||||
switch (action.type) {
|
||||
case AssetAction.ARCHIVE:
|
||||
case AssetAction.DELETE:
|
||||
case AssetAction.TRASH: {
|
||||
case AssetAction.ARCHIVE: {
|
||||
await goto(Route.photos());
|
||||
break;
|
||||
}
|
||||
// no default
|
||||
}
|
||||
};
|
||||
|
||||
const onAssetsDelete = async (assetIds: string[]) => {
|
||||
// Only used for single asset shared link
|
||||
if (assetIds.includes(assets[0].id)) {
|
||||
await goto(Route.photos());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if sharedLink?.allowUpload || assets.length > 1}
|
||||
@@ -132,6 +138,8 @@
|
||||
{/if}
|
||||
</header>
|
||||
{:else if assets.length === 1}
|
||||
<OnEvents {onAssetsDelete} />
|
||||
|
||||
{#await getAssetInfo({ ...authManager.params, id: assets[0].id }) then asset}
|
||||
{#await import('$lib/components/asset-viewer/AssetViewer.svelte') then { default: AssetViewer }}
|
||||
<AssetViewer cursor={{ current: asset }} onAction={handleAction} />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import type { AssetCursor } from '$lib/components/asset-viewer/AssetViewer.svelte';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/Thumbnail.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||
@@ -285,9 +286,7 @@
|
||||
|
||||
const handleAction = async (action: Action) => {
|
||||
switch (action.type) {
|
||||
case AssetAction.ARCHIVE:
|
||||
case AssetAction.DELETE:
|
||||
case AssetAction.TRASH: {
|
||||
case AssetAction.ARCHIVE: {
|
||||
const nextAsset = assetCursor.nextAsset ?? assetCursor.previousAsset;
|
||||
assets.splice(
|
||||
assets.findIndex((currentAsset) => currentAsset.id === action.asset.id),
|
||||
@@ -305,6 +304,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
const onAssetsDelete = async (assetIds: string[]) => {
|
||||
const nextAsset = assetCursor.nextAsset ?? assetCursor.previousAsset;
|
||||
assets = assets.filter((asset) => !assetIds.includes(asset.id));
|
||||
if (assets.length === 0) {
|
||||
return await goto(Route.photos());
|
||||
}
|
||||
if (assetIds.includes(assetCursor.current.id) && nextAsset) {
|
||||
await navigateToAsset(nextAsset);
|
||||
}
|
||||
};
|
||||
|
||||
const assetMouseEventHandler = (asset: TimelineAsset | null) => {
|
||||
if (assetInteraction.selectionActive) {
|
||||
handleSelectAssetCandidates(asset);
|
||||
@@ -338,6 +348,8 @@
|
||||
|
||||
<svelte:document onselectstart={onSelectStart} use:shortcuts={shortcutList} onscroll={() => updateSlidingWindow()} />
|
||||
|
||||
<OnEvents {onAssetsDelete} />
|
||||
|
||||
{#if assets.length > 0}
|
||||
<div
|
||||
style:position="relative"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import type { AssetCursor } from '$lib/components/asset-viewer/AssetViewer.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||
@@ -78,6 +78,14 @@
|
||||
};
|
||||
};
|
||||
|
||||
/** Find the next asset to show or close the viewer */
|
||||
const navigateOrCloseViewer = async (id: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
(await navigateToAsset(assetCursor?.nextAsset)) ||
|
||||
(await navigateToAsset(assetCursor?.previousAsset)) ||
|
||||
(await handleClose(id));
|
||||
};
|
||||
|
||||
//TODO: replace this with async derived in svelte 6
|
||||
$effect(() => {
|
||||
const asset = assetViewerManager.asset;
|
||||
@@ -109,35 +117,20 @@
|
||||
const handleRemoveFromAlbum = async (assetIds: string[]) => {
|
||||
timelineManager.removeAssets(assetIds);
|
||||
|
||||
if (!assetIds.includes(assetCursor.current.id)) {
|
||||
return;
|
||||
if (assetIds.includes(assetCursor.current.id)) {
|
||||
await navigateOrCloseViewer(assetCursor.current.id);
|
||||
}
|
||||
|
||||
// keep the cleanup workflow in viewer by moving to adjacent asset first
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
(await navigateToAsset(assetCursor?.nextAsset)) ||
|
||||
(await navigateToAsset(assetCursor?.previousAsset)) ||
|
||||
(await handleClose(assetCursor.current.id));
|
||||
};
|
||||
|
||||
const handlePreAction = async (action: Action) => {
|
||||
switch (action.type) {
|
||||
case removeAction:
|
||||
case AssetAction.TRASH:
|
||||
case AssetAction.RESTORE:
|
||||
case AssetAction.DELETE:
|
||||
case AssetAction.ARCHIVE:
|
||||
case AssetAction.SET_VISIBILITY_LOCKED:
|
||||
case AssetAction.SET_VISIBILITY_TIMELINE: {
|
||||
// must update manager before performing any navigation
|
||||
timelineManager.removeAssets([action.asset.id]);
|
||||
|
||||
// find the next asset to show or close the viewer
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
(await navigateToAsset(assetCursor?.nextAsset)) ||
|
||||
(await navigateToAsset(assetCursor?.previousAsset)) ||
|
||||
(await handleClose(action.asset.id));
|
||||
|
||||
await navigateOrCloseViewer(action.asset.id);
|
||||
break;
|
||||
}
|
||||
// no default
|
||||
@@ -199,9 +192,19 @@
|
||||
// no default
|
||||
}
|
||||
};
|
||||
const handleUndoDelete = async (assets: TimelineAsset[]) => {
|
||||
timelineManager.upsertAssets(assets);
|
||||
if (assets.length === 0) {
|
||||
|
||||
const onAssetsDelete = async (assetIds: string[]) => {
|
||||
timelineManager.removeAssets(assetIds);
|
||||
|
||||
if (assetIds.includes(assetCursor.current.id)) {
|
||||
await navigateOrCloseViewer(assetCursor.current.id);
|
||||
}
|
||||
};
|
||||
|
||||
const onAssetsRestore = async (assets: AssetResponseDto[]) => {
|
||||
timelineManager.upsertAssets(assets.map((a) => toTimelineAsset(a)));
|
||||
if (assets.length !== 1) {
|
||||
// don't reopen asset viewer if multiple assets were restored (bulk action)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,6 +237,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<OnEvents {onAssetsDelete} {onAssetsRestore} />
|
||||
|
||||
{#await import('$lib/components/asset-viewer/AssetViewer.svelte') then { default: AssetViewer }}
|
||||
<AssetViewer
|
||||
{withStacked}
|
||||
@@ -249,7 +254,6 @@
|
||||
handleAction(action);
|
||||
assetCacheManager.invalidate();
|
||||
}}
|
||||
onUndoDelete={handleUndoDelete}
|
||||
onRandom={handleRandom}
|
||||
onRemoveFromAlbum={handleRemoveFromAlbum}
|
||||
onClose={handleClose}
|
||||
|
||||
@@ -3,9 +3,6 @@ export const UUID_REGEX = /^[\dA-Fa-f]{8}(?:\b-[\dA-Fa-f]{4}){3}\b-[\dA-Fa-f]{12
|
||||
export enum AssetAction {
|
||||
ARCHIVE = 'archive',
|
||||
UNARCHIVE = 'unarchive',
|
||||
TRASH = 'trash',
|
||||
DELETE = 'delete',
|
||||
RESTORE = 'restore',
|
||||
STACK = 'stack',
|
||||
UNSTACK = 'unstack',
|
||||
SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset',
|
||||
|
||||
@@ -36,6 +36,7 @@ export type Events = {
|
||||
AssetUpdate: [AssetResponseDto];
|
||||
AssetsArchive: [string[]];
|
||||
AssetsDelete: [string[]];
|
||||
AssetsRestore: [AssetResponseDto[]];
|
||||
AssetEditsApplied: [string];
|
||||
AssetsTag: [string[]];
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ vitest.mock('$lib/utils', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), function () {
|
||||
return {
|
||||
featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: {} } as never,
|
||||
};
|
||||
});
|
||||
|
||||
describe('AssetService', () => {
|
||||
describe('getAssetActions', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
AssetMediaSize,
|
||||
AssetTypeEnum,
|
||||
AssetVisibility,
|
||||
deleteAssets,
|
||||
getAssetInfo,
|
||||
restoreAssets,
|
||||
runAssetJobs,
|
||||
updateAsset,
|
||||
type AssetJobsDto,
|
||||
@@ -15,12 +17,15 @@ import {
|
||||
mdiCogRefreshOutline,
|
||||
mdiContentCopy,
|
||||
mdiDatabaseRefreshOutline,
|
||||
mdiDeleteForeverOutline,
|
||||
mdiDeleteOutline,
|
||||
mdiDownload,
|
||||
mdiDownloadBox,
|
||||
mdiFaceRecognition,
|
||||
mdiHeadSyncOutline,
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
mdiHistory,
|
||||
mdiImageRefreshOutline,
|
||||
mdiInformationOutline,
|
||||
mdiMagnifyMinusOutline,
|
||||
@@ -34,14 +39,18 @@ import {
|
||||
mdiTune,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
||||
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { getAssetMediaUrl, getSharedLink, sleep } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils';
|
||||
@@ -96,6 +105,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
const sharedLink = getSharedLink();
|
||||
const authUser = authManager.authenticated ? authManager.user : undefined;
|
||||
const isOwner = !!(authUser && authUser.id === asset.ownerId);
|
||||
const isDeletionPermanent = asset.isTrashed || !featureFlagsManager.value.trash;
|
||||
|
||||
const Share: ActionItem = {
|
||||
title: $t('share'),
|
||||
@@ -242,6 +252,29 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
shortcuts: [{ key: 'e' }],
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
title: $t('delete'),
|
||||
icon: mdiDeleteOutline,
|
||||
$if: () => isOwner && !isDeletionPermanent,
|
||||
onAction: () => handleTrashOrDelete(asset),
|
||||
shortcuts: { key: 'Delete' },
|
||||
};
|
||||
|
||||
const PermanentlyDelete: ActionItem = {
|
||||
title: $t('permanently_delete'),
|
||||
icon: mdiDeleteForeverOutline,
|
||||
$if: () => isOwner && isDeletionPermanent,
|
||||
onAction: () => handleTrashOrDelete(asset, true),
|
||||
shortcuts: { key: 'Delete', shift: true },
|
||||
};
|
||||
|
||||
const Restore: ActionItem = {
|
||||
title: $t('restore'),
|
||||
icon: mdiHistory,
|
||||
$if: () => asset.visibility !== AssetVisibility.Locked && asset.isTrashed,
|
||||
onAction: () => handleRestore(asset),
|
||||
};
|
||||
|
||||
const RefreshFacesJob: ActionItem = {
|
||||
title: $t('refresh_faces'),
|
||||
icon: mdiHeadSyncOutline,
|
||||
@@ -286,6 +319,9 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
Tag,
|
||||
TagPeople,
|
||||
Edit,
|
||||
Delete,
|
||||
PermanentlyDelete,
|
||||
Restore,
|
||||
RefreshFacesJob,
|
||||
RefreshMetadataJob,
|
||||
RegenerateThumbnailJob,
|
||||
@@ -367,6 +403,47 @@ const handleUnfavorite = async (asset: AssetResponseDto) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const handleTrashOrDelete = async (asset: AssetResponseDto, force?: boolean) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
if (force && get(showDeleteModal)) {
|
||||
const confirmed = await modalManager.show(AssetDeleteConfirmModal, { size: 1 });
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force } });
|
||||
eventManager.emit('AssetsDelete', [asset.id]);
|
||||
if (force) {
|
||||
toastManager.primary($t('permanently_deleted_asset'));
|
||||
} else {
|
||||
toastManager.primary(
|
||||
{
|
||||
description: $t('moved_to_trash'),
|
||||
button: { label: $t('undo'), color: 'secondary', onclick: () => handleRestore(asset) },
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_asset'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = async (asset: AssetResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
await restoreAssets({ bulkIdsDto: { ids: [asset.id] } });
|
||||
eventManager.emit('AssetsRestore', [asset]);
|
||||
toastManager.primary($t('restored_asset'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_restore_assets'));
|
||||
}
|
||||
};
|
||||
|
||||
const getAssetJobMessage = ($t: MessageFormatter, job: AssetJobName) => {
|
||||
const messages: Record<AssetJobName, string> = {
|
||||
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
|
||||
|
||||
+2
-6
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import LargeAssetData from './LargeAssetData.svelte';
|
||||
@@ -37,16 +36,14 @@
|
||||
return asset;
|
||||
};
|
||||
|
||||
const preAction = async (payload: Action) => {
|
||||
if (payload.type == 'trash') {
|
||||
const onAssetsDelete = async (assetIds: string[]) => {
|
||||
if (assetIds.includes(assetCursor.current.id)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
(await navigateToAsset(assetCursor?.nextAsset)) ||
|
||||
(await navigateToAsset(assetCursor?.previousAsset)) ||
|
||||
assetViewerManager.showAssetViewer(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onAssetsDelete = (assetIds: string[]) => {
|
||||
assets = assets.filter(({ id }) => !assetIds.includes(id));
|
||||
};
|
||||
|
||||
@@ -84,7 +81,6 @@
|
||||
cursor={assetCursor}
|
||||
showNavigation={assets.length > 1}
|
||||
{onRandom}
|
||||
{preAction}
|
||||
onClose={() => {
|
||||
assetViewerManager.showAssetViewer(false);
|
||||
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
||||
|
||||
Reference in New Issue
Block a user