mirror of
https://github.com/immich-app/immich.git
synced 2026-05-01 13:20:46 -04:00
395 lines
12 KiB
TypeScript
395 lines
12 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 AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
|
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
|
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
|
import { user as authUser, preferences } from '$lib/stores/user.store';
|
|
import type { AssetControlContext } from '$lib/types';
|
|
import { getSharedLink, sleep } from '$lib/utils';
|
|
import { downloadUrl } from '$lib/utils/asset-utils';
|
|
import { handleError } from '$lib/utils/handle-error';
|
|
import { getFormatter } from '$lib/utils/i18n';
|
|
import { asQueryString } from '$lib/utils/shared-links';
|
|
import {
|
|
AssetJobName,
|
|
AssetTypeEnum,
|
|
AssetVisibility,
|
|
getAssetInfo,
|
|
getBaseUrl,
|
|
runAssetJobs,
|
|
updateAsset,
|
|
type AssetJobsDto,
|
|
type AssetResponseDto,
|
|
} from '@immich/sdk';
|
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
|
import {
|
|
mdiAlertOutline,
|
|
mdiCogRefreshOutline,
|
|
mdiContentCopy,
|
|
mdiDatabaseRefreshOutline,
|
|
mdiDownload,
|
|
mdiDownloadBox,
|
|
mdiFaceRecognition,
|
|
mdiHeadSyncOutline,
|
|
mdiHeart,
|
|
mdiHeartOutline,
|
|
mdiImageRefreshOutline,
|
|
mdiInformationOutline,
|
|
mdiMagnifyMinusOutline,
|
|
mdiMagnifyPlusOutline,
|
|
mdiMotionPauseOutline,
|
|
mdiMotionPlayOutline,
|
|
mdiPlus,
|
|
mdiShareVariantOutline,
|
|
mdiTagPlusOutline,
|
|
mdiTune,
|
|
} from '@mdi/js';
|
|
import type { MessageFormatter } from 'svelte-i18n';
|
|
import { get } from 'svelte/store';
|
|
|
|
export const getAssetBulkActions = ($t: MessageFormatter, ctx: AssetControlContext) => {
|
|
const ownedAssets = ctx.getOwnedAssets();
|
|
const assetIds = ownedAssets.map((asset) => asset.id);
|
|
const isAllVideos = ownedAssets.every((asset) => asset.isVideo);
|
|
|
|
const onAction = async (name: AssetJobName) => {
|
|
await handleRunAssetJob({ name, assetIds });
|
|
ctx.clearSelect();
|
|
};
|
|
|
|
const AddToAlbum: ActionItem = {
|
|
title: $t('add_to_album'),
|
|
icon: mdiPlus,
|
|
shortcuts: [{ key: 'l' }],
|
|
onAction: () => modalManager.show(AssetAddToAlbumModal, { assetIds }),
|
|
};
|
|
|
|
const RefreshFacesJob: ActionItem = {
|
|
title: $t('refresh_faces'),
|
|
icon: mdiHeadSyncOutline,
|
|
onAction: () => onAction(AssetJobName.RefreshFaces),
|
|
};
|
|
|
|
const RefreshMetadataJob: ActionItem = {
|
|
title: $t('refresh_metadata'),
|
|
icon: mdiDatabaseRefreshOutline,
|
|
onAction: () => onAction(AssetJobName.RefreshMetadata),
|
|
};
|
|
|
|
const RegenerateThumbnailJob: ActionItem = {
|
|
title: $t('refresh_thumbnails'),
|
|
icon: mdiImageRefreshOutline,
|
|
onAction: () => onAction(AssetJobName.RegenerateThumbnail),
|
|
};
|
|
|
|
const TranscodeVideoJob: ActionItem = {
|
|
title: $t('refresh_encoded_videos'),
|
|
icon: mdiCogRefreshOutline,
|
|
onAction: () => onAction(AssetJobName.TranscodeVideo),
|
|
$if: () => isAllVideos,
|
|
};
|
|
|
|
return { AddToAlbum, RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob };
|
|
};
|
|
|
|
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
|
|
const sharedLink = getSharedLink();
|
|
const currentAuthUser = get(authUser);
|
|
const userPreferences = get(preferences);
|
|
const isOwner = !!(currentAuthUser && currentAuthUser.id === asset.ownerId);
|
|
|
|
const Share: ActionItem = {
|
|
title: $t('share'),
|
|
icon: mdiShareVariantOutline,
|
|
type: $t('assets'),
|
|
$if: () => !!(currentAuthUser && !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: () => isOwner || !!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 AddToAlbum: ActionItem = {
|
|
title: $t('add_to_album'),
|
|
icon: mdiPlus,
|
|
shortcuts: [{ key: 'l' }],
|
|
$if: () => asset.visibility !== AssetVisibility.Locked && !asset.isTrashed,
|
|
onAction: () => modalManager.show(AssetAddToAlbumModal, { assetIds: [asset.id] }),
|
|
};
|
|
|
|
const Offline: ActionItem = {
|
|
title: $t('asset_offline'),
|
|
icon: mdiAlertOutline,
|
|
type: $t('assets'),
|
|
color: 'danger',
|
|
$if: () => !!asset.isOffline,
|
|
onAction: () => assetViewerManager.toggleDetailPanel(),
|
|
};
|
|
|
|
const ZoomIn: ActionItem = {
|
|
title: $t('zoom_image'),
|
|
icon: mdiMagnifyPlusOutline,
|
|
$if: () => assetViewerManager.canZoomIn(),
|
|
onAction: () => assetViewerManager.emit('Zoom'),
|
|
};
|
|
|
|
const ZoomOut: ActionItem = {
|
|
title: $t('zoom_image'),
|
|
icon: mdiMagnifyMinusOutline,
|
|
$if: () => assetViewerManager.canZoomOut(),
|
|
onAction: () => assetViewerManager.emit('Zoom'),
|
|
};
|
|
|
|
const Copy: ActionItem = {
|
|
title: $t('copy_image'),
|
|
icon: mdiContentCopy,
|
|
$if: () => assetViewerManager.canCopyImage(),
|
|
onAction: () => assetViewerManager.emit('Copy'),
|
|
};
|
|
|
|
const Info: ActionItem = {
|
|
title: $t('info'),
|
|
icon: mdiInformationOutline,
|
|
type: $t('assets'),
|
|
$if: () => asset.hasMetadata,
|
|
onAction: () => assetViewerManager.toggleDetailPanel(),
|
|
shortcuts: { key: 'i' },
|
|
};
|
|
|
|
const Tag: ActionItem = {
|
|
title: $t('add_tag'),
|
|
icon: mdiTagPlusOutline,
|
|
type: $t('assets'),
|
|
$if: () => userPreferences.tags.enabled,
|
|
onAction: () => modalManager.show(AssetTagModal, { assetIds: [asset.id] }),
|
|
shortcuts: { key: 't' },
|
|
};
|
|
|
|
const TagPeople: ActionItem = {
|
|
title: $t('tag_people'),
|
|
icon: mdiFaceRecognition,
|
|
type: $t('assets'),
|
|
$if: () => isOwner && asset.type === AssetTypeEnum.Image && !asset.isTrashed,
|
|
onAction: () => {
|
|
isFaceEditMode.value = !isFaceEditMode.value;
|
|
},
|
|
shortcuts: { key: 'p' },
|
|
};
|
|
|
|
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: $t('refresh_faces'),
|
|
icon: mdiHeadSyncOutline,
|
|
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshFaces, assetIds: [asset.id] }),
|
|
};
|
|
|
|
const RefreshMetadataJob: ActionItem = {
|
|
title: $t('refresh_metadata'),
|
|
icon: mdiDatabaseRefreshOutline,
|
|
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshMetadata, assetIds: [asset.id] }),
|
|
};
|
|
|
|
const RegenerateThumbnailJob: ActionItem = {
|
|
title: $t('refresh_thumbnails'),
|
|
icon: mdiImageRefreshOutline,
|
|
onAction: () => handleRunAssetJob({ name: AssetJobName.RegenerateThumbnail, assetIds: [asset.id] }),
|
|
};
|
|
|
|
const TranscodeVideoJob: ActionItem = {
|
|
title: $t('refresh_encoded_videos'),
|
|
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,
|
|
AddToAlbum,
|
|
ZoomIn,
|
|
ZoomOut,
|
|
Copy,
|
|
Tag,
|
|
TagPeople,
|
|
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,
|
|
},
|
|
];
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
|
|
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.primary($t('downloading_asset_filename', { values: { filename } }));
|
|
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.primary($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.primary($t('removed_from_favorites'));
|
|
eventManager.emit('AssetUpdate', response);
|
|
} catch (error) {
|
|
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } }));
|
|
}
|
|
};
|
|
|
|
const getAssetJobMessage = ($t: MessageFormatter, 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];
|
|
};
|
|
|
|
const handleRunAssetJob = async (dto: AssetJobsDto) => {
|
|
const $t = await getFormatter();
|
|
|
|
try {
|
|
await runAssetJobs({ assetJobsDto: dto });
|
|
toastManager.primary(getAssetJobMessage($t, dto.name));
|
|
} catch (error) {
|
|
handleError(error, $t('errors.unable_to_submit_job'));
|
|
}
|
|
};
|