Files
immich/web/src/lib/services/asset.service.ts
2026-01-23 14:06:19 -05:00

286 lines
8.9 KiB
TypeScript

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'));
}
};