import { ProjectionType } from '$lib/constants'; 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 SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import { user as authUser, preferences } from '$lib/stores/user.store'; import { getAssetJobName, getSharedLink, sleep } from '$lib/utils'; import { downloadUrl } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; import { asQueryString } from '$lib/utils/shared-links'; import { AssetJobName, AssetTypeEnum, AssetVisibility, copyAsset, deleteAssets, getAssetInfo, getBaseUrl, runAssetJobs, updateAsset, type AssetJobsDto, type AssetResponseDto, } from '@immich/sdk'; import { modalManager, toastManager, type ActionItem } from '@immich/ui'; import { mdiAlertOutline, mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiDownload, mdiDownloadBox, mdiHeadSyncOutline, mdiHeart, mdiHeartOutline, mdiImageRefreshOutline, mdiInformationOutline, mdiMotionPauseOutline, mdiMotionPlayOutline, mdiShareVariantOutline, mdiTune, } from '@mdi/js'; import type { MessageFormatter } from 'svelte-i18n'; import { get } from 'svelte/store'; export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => { const sharedLink = getSharedLink(); const currentAuthUser = get(authUser); const isOwner = !!(currentAuthUser && currentAuthUser.id === asset.ownerId); const Share: ActionItem = { title: $t('share'), icon: mdiShareVariantOutline, type: $t('assets'), $if: () => !!(get(authUser) && !asset.isTrashed && asset.visibility !== AssetVisibility.Locked), onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }), }; const Download: ActionItem = { title: $t('download'), icon: mdiDownload, shortcuts: { key: 'd', shift: true }, type: $t('assets'), $if: () => !!currentAuthUser, onAction: () => handleDownloadAsset(asset, { edited: true }), }; const DownloadOriginal: ActionItem = { title: $t('download_original'), icon: mdiDownloadBox, type: $t('assets'), $if: () => !!currentAuthUser && asset.isEdited, onAction: () => handleDownloadAsset(asset, { edited: false }), }; const SharedLinkDownload: ActionItem = { ...Download, $if: () => !currentAuthUser && sharedLink && sharedLink.allowDownload, }; const PlayMotionPhoto: ActionItem = { title: $t('play_motion_photo'), icon: mdiMotionPlayOutline, type: $t('assets'), $if: () => !!asset.livePhotoVideoId && !assetViewerManager.isPlayingMotionPhoto, onAction: () => { assetViewerManager.isPlayingMotionPhoto = true; }, }; const StopMotionPhoto: ActionItem = { title: $t('stop_motion_photo'), icon: mdiMotionPauseOutline, type: $t('assets'), $if: () => !!asset.livePhotoVideoId && assetViewerManager.isPlayingMotionPhoto, onAction: () => { assetViewerManager.isPlayingMotionPhoto = false; }, }; const Favorite: ActionItem = { title: $t('to_favorite'), icon: mdiHeartOutline, type: $t('assets'), $if: () => isOwner && !asset.isFavorite, onAction: () => handleFavorite(asset), shortcuts: [{ key: 'f' }], }; const Unfavorite: ActionItem = { title: $t('unfavorite'), icon: mdiHeart, type: $t('assets'), $if: () => isOwner && asset.isFavorite, onAction: () => handleUnfavorite(asset), shortcuts: [{ key: 'f' }], }; const Offline: ActionItem = { title: $t('asset_offline'), icon: mdiAlertOutline, type: $t('assets'), color: 'danger', $if: () => !!asset.isOffline, onAction: () => assetViewerManager.toggleDetailPanel(), }; const Info: ActionItem = { title: $t('info'), icon: mdiInformationOutline, type: $t('assets'), $if: () => asset.hasMetadata, onAction: () => assetViewerManager.toggleDetailPanel(), shortcuts: [{ key: 'i' }], }; const Edit: ActionItem = { title: $t('editor'), icon: mdiTune, $if: () => !sharedLink && isOwner && asset.type === AssetTypeEnum.Image && !asset.livePhotoVideoId && asset.exifInfo?.projectionType !== ProjectionType.EQUIRECTANGULAR && !asset.originalPath.toLowerCase().endsWith('.insp') && !asset.originalPath.toLowerCase().endsWith('.gif') && !asset.originalPath.toLowerCase().endsWith('.svg'), onAction: () => assetViewerManager.openEditor(), }; 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 { Share, Download, DownloadOriginal, SharedLinkDownload, Offline, Info, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Edit, RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob, }; }; export const handleDownloadAsset = async (asset: AssetResponseDto, { edited }: { edited: boolean }) => { const $t = await getFormatter(); const assets = [ { filename: asset.originalFileName, id: asset.id, size: asset.exifInfo?.fileSizeInByte || 0, }, ]; const isAndroidMotionVideo = (asset: AssetResponseDto) => { return asset.originalPath.includes('encoded-video'); }; if (asset.livePhotoVideoId) { const motionAsset = await getAssetInfo({ ...authManager.params, id: asset.livePhotoVideoId }); if (!isAndroidMotionVideo(motionAsset) || get(preferences)?.download.includeEmbeddedVideos) { assets.push({ filename: motionAsset.originalFileName, id: asset.livePhotoVideoId, size: motionAsset.exifInfo?.fileSizeInByte || 0, }); } } const queryParams = asQueryString(authManager.params); for (const [i, { filename, id }] of assets.entries()) { if (i !== 0) { // play nice with Safari await sleep(500); } try { toastManager.success($t('downloading_asset_filename', { values: { filename: asset.originalFileName } })); downloadUrl( getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}&edited=${edited}` : `?edited=${edited}`), filename, ); } catch (error) { handleError(error, $t('errors.error_downloading', { values: { filename } })); } } }; const handleFavorite = async (asset: AssetResponseDto) => { const $t = await getFormatter(); try { const response = await updateAsset({ id: asset.id, updateAssetDto: { isFavorite: true } }); toastManager.success($t('added_to_favorites')); eventManager.emit('AssetUpdate', response); } catch (error) { handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } })); } }; const handleUnfavorite = async (asset: AssetResponseDto) => { const $t = await getFormatter(); try { const response = await updateAsset({ id: asset.id, updateAssetDto: { isFavorite: false } }); toastManager.success($t('removed_from_favorites')); eventManager.emit('AssetUpdate', response); } catch (error) { handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } })); } }; export const handleReplaceAsset = async (oldAssetId: string) => { const [newAssetId] = await openFileUploadDialog({ multiple: false }); await copyAsset({ assetCopyDto: { sourceId: oldAssetId, targetId: newAssetId } }); await deleteAssets({ assetBulkDeleteDto: { ids: [oldAssetId], force: true } }); 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')); } };