refactor(web): asset job actions (#25426)

This commit is contained in:
Jason Rasmussen 2026-01-21 13:13:16 -05:00 committed by GitHub
parent dc82c13ddc
commit 1b032339aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 90 additions and 74 deletions

View file

@ -16,7 +16,7 @@ describe('AssetViewerNavBar component', () => {
preAction: () => {}, preAction: () => {},
onZoomImage: () => {}, onZoomImage: () => {},
onAction: () => {}, onAction: () => {},
onRunJob: () => {}, onEdit: () => {},
onPlaySlideshow: () => {}, onPlaySlideshow: () => {},
onClose: () => {}, onClose: () => {},
playOriginalVideo: false, playOriginalVideo: false,

View file

@ -28,12 +28,11 @@
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetJobName, getSharedLink, withoutIcons } from '$lib/utils'; import { getSharedLink, withoutIcons } from '$lib/utils';
import type { OnUndoDelete } from '$lib/utils/actions'; import type { OnUndoDelete } from '$lib/utils/actions';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { import {
AssetJobName,
AssetTypeEnum, AssetTypeEnum,
AssetVisibility, AssetVisibility,
type AlbumResponseDto, type AlbumResponseDto,
@ -44,13 +43,9 @@
import { CommandPaletteDefaultProvider, IconButton, type ActionItem } from '@immich/ui'; import { CommandPaletteDefaultProvider, IconButton, type ActionItem } from '@immich/ui';
import { import {
mdiArrowLeft, mdiArrowLeft,
mdiCogRefreshOutline,
mdiCompare, mdiCompare,
mdiContentCopy, mdiContentCopy,
mdiDatabaseRefreshOutline,
mdiDotsVertical, mdiDotsVertical,
mdiHeadSyncOutline,
mdiImageRefreshOutline,
mdiImageSearch, mdiImageSearch,
mdiMagnifyMinusOutline, mdiMagnifyMinusOutline,
mdiMagnifyPlusOutline, mdiMagnifyPlusOutline,
@ -71,7 +66,6 @@
preAction: PreAction; preAction: PreAction;
onAction: OnAction; onAction: OnAction;
onUndoDelete?: OnUndoDelete; onUndoDelete?: OnUndoDelete;
onRunJob: (name: AssetJobName) => void;
onPlaySlideshow: () => void; onPlaySlideshow: () => void;
onEdit: () => void; onEdit: () => void;
onClose?: () => void; onClose?: () => void;
@ -90,7 +84,6 @@
preAction, preAction,
onAction, onAction,
onUndoDelete = undefined, onUndoDelete = undefined,
onRunJob,
onPlaySlideshow, onPlaySlideshow,
onClose, onClose,
onEdit, onEdit,
@ -124,6 +117,10 @@
PlayMotionPhoto, PlayMotionPhoto,
StopMotionPhoto, StopMotionPhoto,
Info, Info,
RefreshFacesJob,
RefreshMetadataJob,
RegenerateThumbnailJob,
TranscodeVideoJob,
} = $derived(getAssetActions($t, asset)); } = $derived(getAssetActions($t, asset));
const sharedLink = getSharedLink(); const sharedLink = getSharedLink();
@ -140,7 +137,24 @@
<CommandPaletteDefaultProvider <CommandPaletteDefaultProvider
name={$t('assets')} name={$t('assets')}
actions={withoutIcons([Close, Share, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info])} actions={withoutIcons([
Close,
Cast,
Share,
Download,
DownloadOriginal,
SharedLinkDownload,
Offline,
Favorite,
Unfavorite,
PlayMotionPhoto,
StopMotionPhoto,
Info,
RefreshFacesJob,
RefreshMetadataJob,
RegenerateThumbnailJob,
TranscodeVideoJob,
])}
/> />
<div <div
@ -275,28 +289,10 @@
{/if} {/if}
{#if isOwner} {#if isOwner}
<hr /> <hr />
<MenuOption <ActionMenuItem action={RefreshFacesJob} />
icon={mdiHeadSyncOutline} <ActionMenuItem action={RefreshMetadataJob} />
onClick={() => onRunJob(AssetJobName.RefreshFaces)} <ActionMenuItem action={RegenerateThumbnailJob} />
text={$getAssetJobName(AssetJobName.RefreshFaces)} <ActionMenuItem action={TranscodeVideoJob} />
/>
<MenuOption
icon={mdiDatabaseRefreshOutline}
onClick={() => onRunJob(AssetJobName.RefreshMetadata)}
text={$getAssetJobName(AssetJobName.RefreshMetadata)}
/>
<MenuOption
icon={mdiImageRefreshOutline}
onClick={() => onRunJob(AssetJobName.RegenerateThumbnail)}
text={$getAssetJobName(AssetJobName.RegenerateThumbnail)}
/>
{#if asset.type === AssetTypeEnum.Video}
<MenuOption
icon={mdiCogRefreshOutline}
onClick={() => onRunJob(AssetJobName.TranscodeVideo)}
text={$getAssetJobName(AssetJobName.TranscodeVideo)}
/>
{/if}
{/if} {/if}
</ButtonContextMenu> </ButtonContextMenu>
{/if} {/if}

View file

@ -19,7 +19,7 @@
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store'; import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { getAssetJobMessage, getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils'; import { getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils';
import type { OnUndoDelete } from '$lib/utils/actions'; import type { OnUndoDelete } from '$lib/utils/actions';
import { navigateToAsset } from '$lib/utils/asset-utils'; import { navigateToAsset } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@ -28,18 +28,15 @@
import { preloadImageUrl } from '$lib/utils/sw-messaging'; import { preloadImageUrl } from '$lib/utils/sw-messaging';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { import {
AssetJobName,
AssetTypeEnum, AssetTypeEnum,
getAllAlbums, getAllAlbums,
getAssetInfo, getAssetInfo,
getStack, getStack,
runAssetJobs,
type AlbumResponseDto, type AlbumResponseDto,
type AssetResponseDto, type AssetResponseDto,
type PersonResponseDto, type PersonResponseDto,
type StackResponseDto, type StackResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { onDestroy, onMount, untrack } from 'svelte'; import { onDestroy, onMount, untrack } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@ -262,15 +259,6 @@
isShowEditor = !isShowEditor; isShowEditor = !isShowEditor;
}; };
const handleRunJob = async (name: AssetJobName) => {
try {
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
toastManager.success($getAssetJobMessage(name));
} catch (error) {
handleError(error, $t('errors.unable_to_submit_job'));
}
};
/** /**
* Slide show mode * Slide show mode
*/ */
@ -473,7 +461,6 @@
onAction={handleAction} onAction={handleAction}
{onUndoDelete} {onUndoDelete}
onEdit={showEditor} onEdit={showEditor}
onRunJob={handleRunJob}
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
onClose={onClose ? () => onClose(asset) : undefined} onClose={onClose ? () => onClose(asset) : undefined}
{playOriginalVideo} {playOriginalVideo}

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils'; import { getAssetJobIcon, getAssetJobName } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { AssetJobName, runAssetJobs } from '@immich/sdk'; import { AssetJobName, runAssetJobs } from '@immich/sdk';
import { toastManager } from '@immich/ui'; import { toastManager } from '@immich/ui';
@ -22,7 +22,7 @@
try { try {
const ids = [...getOwnedAssets()].map(({ id }) => id); const ids = [...getOwnedAssets()].map(({ id }) => id);
await runAssetJobs({ assetJobsDto: { assetIds: ids, name } }); await runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
toastManager.success($getAssetJobMessage(name)); toastManager.success(getAssetJobName($t, name));
clearSelect(); clearSelect();
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_submit_job')); handleError(error, $t('errors.unable_to_submit_job'));
@ -32,6 +32,6 @@
{#each jobs as job (job)} {#each jobs as job (job)}
{#if isAllVideos || job !== AssetJobName.TranscodeVideo} {#if isAllVideos || job !== AssetJobName.TranscodeVideo}
<MenuOption text={$getAssetJobName(job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} /> <MenuOption text={getAssetJobName($t, job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} />
{/if} {/if}
{/each} {/each}

View file

@ -3,28 +3,36 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { user as authUser, preferences } from '$lib/stores/user.store'; import { user as authUser, preferences } from '$lib/stores/user.store';
import { getSharedLink, sleep } from '$lib/utils'; import { getAssetJobName, getSharedLink, sleep } from '$lib/utils';
import { downloadUrl } from '$lib/utils/asset-utils'; import { downloadUrl } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { asQueryString } from '$lib/utils/shared-links'; import { asQueryString } from '$lib/utils/shared-links';
import { import {
AssetJobName,
AssetTypeEnum,
AssetVisibility, AssetVisibility,
copyAsset, copyAsset,
deleteAssets, deleteAssets,
getAssetInfo, getAssetInfo,
getBaseUrl, getBaseUrl,
runAssetJobs,
updateAsset, updateAsset,
type AssetJobsDto,
type AssetResponseDto, type AssetResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui'; import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { import {
mdiAlertOutline, mdiAlertOutline,
mdiCogRefreshOutline,
mdiDatabaseRefreshOutline,
mdiDownload, mdiDownload,
mdiDownloadBox, mdiDownloadBox,
mdiHeadSyncOutline,
mdiHeart, mdiHeart,
mdiHeartOutline, mdiHeartOutline,
mdiImageRefreshOutline,
mdiInformationOutline, mdiInformationOutline,
mdiMotionPauseOutline, mdiMotionPauseOutline,
mdiMotionPlayOutline, mdiMotionPlayOutline,
@ -124,6 +132,31 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
shortcuts: [{ key: 'i' }], shortcuts: [{ key: 'i' }],
}; };
const RefreshFacesJob: ActionItem = {
title: getAssetJobName($t, AssetJobName.RefreshFaces),
icon: mdiHeadSyncOutline,
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshFaces, assetIds: [asset.id] }),
};
const RefreshMetadataJob: ActionItem = {
title: getAssetJobName($t, AssetJobName.RefreshMetadata),
icon: mdiDatabaseRefreshOutline,
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshMetadata, assetIds: [asset.id] }),
};
const RegenerateThumbnailJob: ActionItem = {
title: getAssetJobName($t, AssetJobName.RegenerateThumbnail),
icon: mdiImageRefreshOutline,
onAction: () => handleRunAssetJob({ name: AssetJobName.RegenerateThumbnail, assetIds: [asset.id] }),
};
const TranscodeVideoJob: ActionItem = {
title: getAssetJobName($t, AssetJobName.TranscodeVideo),
icon: mdiCogRefreshOutline,
onAction: () => handleRunAssetJob({ name: AssetJobName.TranscodeVideo, assetIds: [asset.id] }),
$if: () => asset.type === AssetTypeEnum.Video,
};
return { return {
Share, Share,
Download, Download,
@ -135,6 +168,10 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
Unfavorite, Unfavorite,
PlayMotionPhoto, PlayMotionPhoto,
StopMotionPhoto, StopMotionPhoto,
RefreshFacesJob,
RefreshMetadataJob,
RegenerateThumbnailJob,
TranscodeVideoJob,
}; };
}; };
@ -217,3 +254,14 @@ export const handleReplaceAsset = async (oldAssetId: string) => {
eventManager.emit('AssetReplace', { oldAssetId, newAssetId }); eventManager.emit('AssetReplace', { oldAssetId, newAssetId });
}; };
const handleRunAssetJob = async (dto: AssetJobsDto) => {
const $t = await getFormatter();
try {
await runAssetJobs({ assetJobsDto: dto });
toastManager.success(getAssetJobName($t, dto.name));
} catch (error) {
handleError(error, $t('errors.unable_to_submit_job'));
}
};

View file

@ -28,7 +28,7 @@ import {
} from '@immich/sdk'; } from '@immich/sdk';
import { toastManager, type ActionItem, type IfLike } from '@immich/ui'; import { toastManager, type ActionItem, type IfLike } from '@immich/ui';
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js'; import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
import { init, register, t } from 'svelte-i18n'; import { init, register, t, type MessageFormatter } from 'svelte-i18n';
import { derived, get } from 'svelte/store'; import { derived, get } from 'svelte/store';
interface DownloadRequestOptions<T = unknown> { interface DownloadRequestOptions<T = unknown> {
@ -259,31 +259,16 @@ export const getProfileImageUrl = (user: UserResponseDto) =>
export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) => export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt }); createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
export const getAssetJobName = derived(t, ($t) => { export const getAssetJobName = ($t: MessageFormatter, job: AssetJobName) => {
return (job: AssetJobName) => { const messages: Record<AssetJobName, string> = {
const names: Record<AssetJobName, string> = { [AssetJobName.RefreshFaces]: $t('refreshing_faces'),
[AssetJobName.RefreshFaces]: $t('refresh_faces'), [AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
[AssetJobName.RefreshMetadata]: $t('refresh_metadata'), [AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
[AssetJobName.RegenerateThumbnail]: $t('refresh_thumbnails'), [AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
[AssetJobName.TranscodeVideo]: $t('refresh_encoded_videos'),
};
return names[job];
}; };
});
export const getAssetJobMessage = derived(t, ($t) => { return messages[job];
return (job: AssetJobName) => { };
const messages: Record<AssetJobName, string> = {
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
[AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
[AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
[AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
};
return messages[job];
};
});
export const getAssetJobIcon = (job: AssetJobName) => { export const getAssetJobIcon = (job: AssetJobName) => {
const names: Record<AssetJobName, string> = { const names: Record<AssetJobName, string> = {