diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index deeb89c5c3..62216a750c 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -2,6 +2,7 @@ import { shortcut } from '$lib/actions/shortcut'; import AlbumMap from '$lib/components/album-page/album-map.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; + import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets-store.svelte'; @@ -16,7 +17,6 @@ import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import DownloadAction from '../photos-page/actions/download-action.svelte'; import AssetGrid from '../photos-page/asset-grid.svelte'; - import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte'; import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte'; import ThemeButton from '../shared-components/theme-button.svelte'; diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts index d85325b59a..0918c86bfe 100644 --- a/web/src/lib/components/asset-viewer/actions/action.ts +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -1,20 +1,21 @@ import type { AssetAction } from '$lib/constants'; -import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; +import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; +import type { AlbumResponseDto } from '@immich/sdk'; type ActionMap = { - [AssetAction.ARCHIVE]: { asset: AssetResponseDto }; - [AssetAction.UNARCHIVE]: { asset: AssetResponseDto }; - [AssetAction.FAVORITE]: { asset: AssetResponseDto }; - [AssetAction.UNFAVORITE]: { asset: AssetResponseDto }; - [AssetAction.TRASH]: { asset: AssetResponseDto }; - [AssetAction.DELETE]: { asset: AssetResponseDto }; - [AssetAction.RESTORE]: { asset: AssetResponseDto }; - [AssetAction.ADD]: { asset: AssetResponseDto }; - [AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto }; - [AssetAction.UNSTACK]: { assets: AssetResponseDto[] }; - [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto }; - [AssetAction.SET_VISIBILITY_LOCKED]: { asset: AssetResponseDto }; - [AssetAction.SET_VISIBILITY_TIMELINE]: { asset: AssetResponseDto }; + [AssetAction.ARCHIVE]: { asset: TimelineAsset }; + [AssetAction.UNARCHIVE]: { asset: TimelineAsset }; + [AssetAction.FAVORITE]: { asset: TimelineAsset }; + [AssetAction.UNFAVORITE]: { asset: TimelineAsset }; + [AssetAction.TRASH]: { asset: TimelineAsset }; + [AssetAction.DELETE]: { asset: TimelineAsset }; + [AssetAction.RESTORE]: { asset: TimelineAsset }; + [AssetAction.ADD]: { asset: TimelineAsset }; + [AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto }; + [AssetAction.UNSTACK]: { assets: TimelineAsset[] }; + [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset }; + [AssetAction.SET_VISIBILITY_LOCKED]: { asset: TimelineAsset }; + [AssetAction.SET_VISIBILITY_TIMELINE]: { asset: TimelineAsset }; }; export type Action = { diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte index 202f0e4593..4ebe9d002a 100644 --- a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte @@ -6,6 +6,7 @@ import Portal from '$lib/components/shared-components/portal/portal.svelte'; import { AssetAction } from '$lib/constants'; import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -24,14 +25,14 @@ showSelectionModal = false; const album = await addAssetsToNewAlbum(albumName, [asset.id]); if (album) { - onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album }); + onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album }); } }; const handleAddToAlbum = async (album: AlbumResponseDto) => { showSelectionModal = false; await addAssetsToAlbum(album.id, [asset.id]); - onAction({ type: AssetAction.ADD_TO_ALBUM, asset, album }); + onAction({ type: AssetAction.ADD_TO_ALBUM, asset: toTimelineAsset(asset), album }); }; diff --git a/web/src/lib/components/asset-viewer/actions/archive-action.svelte b/web/src/lib/components/asset-viewer/actions/archive-action.svelte index ed19dff864..362a0a693a 100644 --- a/web/src/lib/components/asset-viewer/actions/archive-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/archive-action.svelte @@ -4,6 +4,7 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import { AssetAction } from '$lib/constants'; import { toggleArchive } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AssetResponseDto } from '@immich/sdk'; import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -18,11 +19,11 @@ const onArchive = async () => { if (!asset.isArchived) { - preAction({ type: AssetAction.ARCHIVE, asset }); + preAction({ type: AssetAction.ARCHIVE, asset: toTimelineAsset(asset) }); } const updatedAsset = await toggleArchive(asset); if (updatedAsset) { - onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset }); + onAction({ type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: toTimelineAsset(asset) }); } }; diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.svelte b/web/src/lib/components/asset-viewer/actions/delete-action.svelte index 24ba2c845d..90322c00f0 100644 --- a/web/src/lib/components/asset-viewer/actions/delete-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/delete-action.svelte @@ -11,6 +11,7 @@ import { showDeleteModal } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { deleteAssets, type AssetResponseDto } from '@immich/sdk'; import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -42,9 +43,9 @@ const trashAsset = async () => { try { - preAction({ type: AssetAction.TRASH, asset }); + preAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } }); - onAction({ type: AssetAction.TRASH, asset }); + onAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); notificationController.show({ message: $t('moved_to_trash'), @@ -58,7 +59,7 @@ const deleteAsset = async () => { try { await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id], force: true } }); - onAction({ type: AssetAction.DELETE, asset }); + onAction({ type: AssetAction.DELETE, asset: toTimelineAsset(asset) }); notificationController.show({ message: $t('permanently_deleted_asset'), diff --git a/web/src/lib/components/asset-viewer/actions/download-action.svelte b/web/src/lib/components/asset-viewer/actions/download-action.svelte index d7f4f56352..c32766a725 100644 --- a/web/src/lib/components/asset-viewer/actions/download-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/download-action.svelte @@ -2,19 +2,21 @@ import { shortcut } from '$lib/actions/shortcut'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; + import { authManager } from '$lib/managers/auth-manager.svelte'; + import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { downloadFile } from '$lib/utils/asset-utils'; - import type { AssetResponseDto } from '@immich/sdk'; + import { getAssetInfo } from '@immich/sdk'; import { mdiFolderDownloadOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; interface Props { - asset: AssetResponseDto; + asset: TimelineAsset; menuItem?: boolean; } let { asset, menuItem = false }: Props = $props(); - const onDownloadFile = () => downloadFile(asset); + const onDownloadFile = async () => downloadFile(await getAssetInfo({ id: asset.id, key: authManager.key })); diff --git a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte index 0cc3188d51..bb1a9343d9 100644 --- a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte @@ -7,6 +7,7 @@ } from '$lib/components/shared-components/notification/notification'; import { AssetAction } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { updateAsset, type AssetResponseDto } from '@immich/sdk'; import { mdiHeart, mdiHeartOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -30,7 +31,10 @@ asset = { ...asset, isFavorite: data.isFavorite }; - onAction({ type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE, asset }); + onAction({ + type: asset.isFavorite ? AssetAction.FAVORITE : AssetAction.UNFAVORITE, + asset: toTimelineAsset(asset), + }); notificationController.show({ type: NotificationType.Info, diff --git a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte index 090e87f4a9..80dfb35067 100644 --- a/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte +++ b/web/src/lib/components/asset-viewer/actions/keep-this-delete-others.svelte @@ -3,6 +3,7 @@ import { AssetAction } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import { keepThisDeleteOthers } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AssetResponseDto, StackResponseDto } from '@immich/sdk'; import { mdiPinOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -29,7 +30,7 @@ const keptAsset = await keepThisDeleteOthers(asset, stack); if (keptAsset) { - onAction({ type: AssetAction.UNSTACK, assets: [keptAsset] }); + onAction({ type: AssetAction.UNSTACK, assets: [toTimelineAsset(keptAsset)] }); } }; diff --git a/web/src/lib/components/asset-viewer/actions/restore-action.svelte b/web/src/lib/components/asset-viewer/actions/restore-action.svelte index abcae5c4c9..c790dab853 100644 --- a/web/src/lib/components/asset-viewer/actions/restore-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/restore-action.svelte @@ -6,6 +6,7 @@ } from '$lib/components/shared-components/notification/notification'; 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 { mdiHistory } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -23,7 +24,7 @@ await restoreAssets({ bulkIdsDto: { ids: [asset.id] } }); asset.isTrashed = false; - onAction({ type: AssetAction.RESTORE, asset }); + onAction({ type: AssetAction.RESTORE, asset: toTimelineAsset(asset) }); notificationController.show({ type: NotificationType.Info, diff --git a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte index 6a7f6d3078..d133010af7 100644 --- a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte @@ -3,14 +3,15 @@ import { AssetAction } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; + import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { handleError } from '$lib/utils/handle-error'; - import { AssetVisibility, updateAssets, Visibility, type AssetResponseDto } from '@immich/sdk'; + import { AssetVisibility, updateAssets, Visibility } from '@immich/sdk'; import { mdiEyeOffOutline, mdiFolderMoveOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { OnAction, PreAction } from './action'; interface Props { - asset: AssetResponseDto; + asset: TimelineAsset; onAction: OnAction; preAction: PreAction; } diff --git a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte index f2a50cce13..1adeead05f 100644 --- a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte @@ -2,6 +2,7 @@ import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import { AssetAction } from '$lib/constants'; import { deleteStack } from '$lib/utils/asset-utils'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { StackResponseDto } from '@immich/sdk'; import { mdiImageMinusOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -17,7 +18,7 @@ const handleUnstack = async () => { const unstackedAssets = await deleteStack([stack.id]); if (unstackedAssets) { - onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets }); + onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets.map((asset) => toTimelineAsset(asset)) }); } }; diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index a25ea6bf90..f77fbc7f20 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -13,6 +13,7 @@ describe('AssetViewerNavBar component', () => { showDownloadButton: false, showMotionPlayButton: false, showShareButton: false, + preAction: () => {}, onZoomImage: () => {}, onCopyImage: () => {}, onAction: () => {}, diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 9436dc13c8..9a52067feb 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -25,6 +25,7 @@ import { getAssetJobName, getSharedLink } from '$lib/utils'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetJobName, AssetTypeEnum, @@ -138,7 +139,7 @@ {/if} {#if !isOwner && showDownloadButton} - + {/if} {#if showDetailButton} @@ -166,7 +167,7 @@ {/if} {#if showDownloadButton} - + {/if} {#if !isLocked} @@ -210,7 +211,7 @@ {/if} {#if !asset.isTrashed} - + {/if}
void; + onClose: (asset: AssetResponseDto) => void; onNext: () => Promise; onPrevious: () => Promise; - onRandom: () => Promise; + onRandom: () => Promise<{ id: string } | undefined>; copyImage?: () => Promise; } @@ -81,7 +83,7 @@ copyImage = $bindable(), }: Props = $props(); - const { setAsset } = assetViewingStore; + const { setAssetId } = assetViewingStore; const { restartProgress: restartSlideshowProgress, stopProgress: stopSlideshowProgress, @@ -121,7 +123,7 @@ untrack(() => { if (stack && stack?.assets.length > 1) { - preloadAssets.push(stack.assets[1]); + preloadAssets.push(toTimelineAsset(stack.assets[1])); } }); }; @@ -161,7 +163,7 @@ slideshowStateUnsubscribe = slideshowState.subscribe((value) => { if (value === SlideshowState.PlaySlideshow) { slideshowHistory.reset(); - slideshowHistory.queue(asset); + slideshowHistory.queue(toTimelineAsset(asset)); handlePromiseError(handlePlaySlideshow()); } else if (value === SlideshowState.StopSlideshow) { handlePromiseError(handleStopSlideshow()); @@ -171,7 +173,7 @@ shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => { if (value === SlideshowNavigation.Shuffle) { slideshowHistory.reset(); - slideshowHistory.queue(asset); + slideshowHistory.queue(toTimelineAsset(asset)); } }); @@ -225,7 +227,7 @@ }; const closeViewer = () => { - onClose({ asset }); + onClose(asset); }; const closeEditor = () => { @@ -292,8 +294,7 @@ let assetViewerHtmlElement = $state(); const slideshowHistory = new SlideshowHistory((asset) => { - setAsset(asset); - $restartSlideshowProgress = true; + handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true))); }); const handleVideoStarted = () => { @@ -563,8 +564,8 @@ imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }} brokenAssetClass="text-xs" dimmed={stackedAsset.id !== asset.id} - asset={stackedAsset} - onClick={(stackedAsset) => { + asset={toTimelineAsset(stackedAsset)} + onClick={() => { asset = stackedAsset; }} onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte index 9c4b0bcaa4..a264ad8ddd 100644 --- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte @@ -12,6 +12,7 @@ resetGlobalCropStore, rotateDegrees, } from '$lib/stores/asset-editor.store'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import type { AssetResponseDto } from '@immich/sdk'; import { animateCropChange, recalculateCrop } from './crop-settings'; import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store'; @@ -81,7 +82,7 @@ aria-label="Crop area" type="button" > - {$getAltText(asset)} + {$getAltText(toTimelineAsset(asset))}
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 6711d126ca..564cef5308 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -3,7 +3,7 @@ import { zoomImageAction, zoomed } from '$lib/actions/zoom-image'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import BrokenAsset from '$lib/components/assets/broken-asset.svelte'; - import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; + import { photoViewerImgElement, type TimelineAsset } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; @@ -13,9 +13,10 @@ import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { getBoundingBox } from '$lib/utils/people-utils'; - import { cancelImageUrl, preloadImageUrl } from '$lib/utils/sw-messaging'; + import { cancelImageUrl } from '$lib/utils/sw-messaging'; import { getAltText } from '$lib/utils/thumbnail-util'; - import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; + import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; import { onDestroy, onMount } from 'svelte'; import { swipe, type SwipeCustomEvent } from 'svelte-gestures'; import { t } from 'svelte-i18n'; @@ -25,7 +26,7 @@ interface Props { asset: AssetResponseDto; - preloadAssets?: AssetResponseDto[] | undefined; + preloadAssets?: TimelineAsset[] | undefined; element?: HTMLDivElement | undefined; haveFadeTransition?: boolean; sharedLink?: SharedLinkResponseDto | undefined; @@ -69,10 +70,11 @@ $boundingBoxesArray = []; }); - const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: AssetResponseDto[]) => { + const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: TimelineAsset[]) => { for (const preloadAsset of preloadAssets || []) { - if (preloadAsset.type === AssetTypeEnum.Image) { - preloadImageUrl(getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash)); + if (preloadAsset.isImage) { + let img = new Image(); + img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash); } } }; @@ -197,7 +199,7 @@ bind:clientWidth={containerWidth} bind:clientHeight={containerHeight} > - {$getAltText(asset)} + {#if !imageLoaded}
@@ -213,7 +215,7 @@ {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} {$getAltText(asset)} @@ -221,7 +223,7 @@ {$getAltText(asset)} void) | undefined; - onSelect?: ((asset: AssetResponseDto) => void) | undefined; - onMouseEvent?: ((event: { isMouseOver: boolean; selectedGroupIndex: number }) => void) | undefined; - handleFocus?: (() => void) | undefined; + onClick?: (asset: TimelineAsset) => void; + onSelect?: (asset: TimelineAsset) => void; + onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void; + handleFocus?: () => void; } let { @@ -290,13 +291,13 @@
{/if} - {#if !authManager.key && showArchiveIcon && asset.isArchived} + {#if !authManager.key && showArchiveIcon && asset.visibility === Visibility.Archive}
{/if} - {#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR} + {#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR}
@@ -309,7 +310,7 @@
@@ -329,17 +330,17 @@ curve={selected} onComplete={(errored) => ((loaded = true), (thumbError = errored))} /> - {#if asset.type === AssetTypeEnum.Video} + {#if asset.isVideo}
- {:else if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId} + {:else if asset.isImage && asset.livePhotoVideoId}
(undefined); + let currentMemoryAssetFull = $derived.by(async () => + current?.asset ? await getAssetInfo({ id: current.asset.id, key: authManager.key }) : undefined, + ); + let currentTimelineAssets = $derived(current?.memory.assets.map((asset) => toTimelineAsset(asset)) || []); + let isSaved = $derived(current?.memory.isSaved); let viewerHeight = $state(0); @@ -77,8 +84,8 @@ const assetInteraction = new AssetInteraction(); let progressBarController: Tween | undefined = $state(undefined); let videoPlayer: HTMLVideoElement | undefined = $state(); - const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`; - const handleNavigate = async (asset?: AssetResponseDto) => { + const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`; + const handleNavigate = async (asset?: { id: string }) => { if ($isViewing) { return asset; } @@ -89,9 +96,9 @@ await goto(asHref(asset)); }; - const setProgressDuration = (asset: AssetResponseDto) => { - if (asset.type === AssetTypeEnum.Video) { - const timeParts = asset.duration.split(':').map(Number); + const setProgressDuration = (asset: TimelineAsset) => { + if (asset.isVideo) { + const timeParts = asset.duration!.split(':').map(Number); const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000; progressBarController = new Tween(0, { duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0), @@ -107,7 +114,8 @@ const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]); const handleEscape = async () => goto(AppRoute.PHOTOS); - const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []); + const handleSelectAll = () => + assetInteraction.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []); const handleAction = async (callingContext: string, action: 'reset' | 'pause' | 'play') => { // leaving these log statements here as comments. Very useful to figure out what's going on during dev! // console.log(`handleAction[${callingContext}] called with: ${action}`); @@ -240,7 +248,7 @@ }; const initPlayer = () => { - const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.type === AssetTypeEnum.Video && !videoPlayer; + const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.isVideo && !videoPlayer; if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) { return; } @@ -441,7 +449,7 @@
{#key current.asset.id}
- {#if current.asset.type === AssetTypeEnum.Video} + {#if current.asset.isVideo}
@@ -623,7 +633,7 @@ import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import type { OnArchive } from '$lib/utils/actions'; + import { archiveAssets } from '$lib/utils/asset-utils'; + import { AssetVisibility, Visibility } from '@immich/sdk'; import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js'; + import { t } from 'svelte-i18n'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; - import { archiveAssets } from '$lib/utils/asset-utils'; - import { t } from 'svelte-i18n'; interface Props { onArchive?: OnArchive; @@ -23,10 +24,10 @@ const { clearSelect, getOwnedAssets } = getAssetControlContext(); const handleArchive = async () => { - const isArchived = !unarchive; - const assets = [...getOwnedAssets()].filter((asset) => asset.isArchived !== isArchived); + const isArchived = unarchive ? Visibility.Timeline : Visibility.Archive; + const assets = [...getOwnedAssets()].filter((asset) => asset.visibility !== isArchived); loading = true; - const ids = await archiveAssets(assets, isArchived); + const ids = await archiveAssets(assets, isArchived as unknown as AssetVisibility); if (ids) { onArchive?.(ids, isArchived); clearSelect(); diff --git a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte index 89c0b42165..5676ad5fbf 100644 --- a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte +++ b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte @@ -6,9 +6,9 @@ } from '$lib/components/shared-components/notification/notification'; import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; - import { AssetJobName, AssetTypeEnum, runAssetJobs } from '@immich/sdk'; - import { getAssetControlContext } from '../asset-select-control-bar.svelte'; + import { AssetJobName, runAssetJobs } from '@immich/sdk'; import { t } from 'svelte-i18n'; + import { getAssetControlContext } from '../asset-select-control-bar.svelte'; interface Props { jobs?: AssetJobName[]; @@ -19,7 +19,7 @@ const { clearSelect, getOwnedAssets } = getAssetControlContext(); - let isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video)); + const isAllVideos = $derived([...getOwnedAssets()].every((asset) => asset.isVideo)); const handleRunJob = async (name: AssetJobName) => { try { diff --git a/web/src/lib/components/photos-page/actions/change-date-action.svelte b/web/src/lib/components/photos-page/actions/change-date-action.svelte index 3232cbd2b4..5f65fdd744 100644 --- a/web/src/lib/components/photos-page/actions/change-date-action.svelte +++ b/web/src/lib/components/photos-page/actions/change-date-action.svelte @@ -4,11 +4,11 @@ import { getSelectedAssets } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { updateAssets } from '@immich/sdk'; + import { mdiCalendarEditOutline } from '@mdi/js'; import { DateTime } from 'luxon'; + import { t } from 'svelte-i18n'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; - import { mdiCalendarEditOutline } from '@mdi/js'; - import { t } from 'svelte-i18n'; interface Props { menuItem?: boolean; } diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte index 1651936c08..df079e45b2 100644 --- a/web/src/lib/components/photos-page/actions/download-action.svelte +++ b/web/src/lib/components/photos-page/actions/download-action.svelte @@ -1,11 +1,14 @@ @@ -94,14 +97,14 @@ clearSelect={() => cancelMultiselect(assetInteraction)} > - + cancelMultiselect(assetInteraction)} /> cancelMultiselect(assetInteraction)} shared /> { + onFavorite={function handleFavoriteUpdate(ids, isFavorite) { if (data.pathAssets && data.pathAssets.length > 0) { for (const id of ids) { const asset = data.pathAssets.find((asset) => asset.id === id); @@ -141,17 +144,17 @@ icons={{ default: mdiFolderOutline, active: mdiFolder }} items={tree} active={currentPath} - {getLink} + getLink={getLinkForPath} />
{/snippet} - +
- + {#if data.pathAssets && data.pathAssets.length > 0} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index ea726d783a..da64314ecf 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -35,7 +35,7 @@ import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { AssetStore } from '$lib/stores/assets-store.svelte'; + import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte'; import { locale } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; @@ -47,7 +47,6 @@ getPersonStatistics, searchPerson, updatePerson, - type AssetResponseDto, type PersonResponseDto, } from '@immich/sdk'; import { @@ -204,7 +203,7 @@ data = { ...data, person }; }; - const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => { + const handleSelectFeaturePhoto = async (asset: TimelineAsset) => { if (viewMode !== PersonPageViewMode.SELECT_PERSON) { return; } diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 162beaf8f5..82816b36b4 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -34,7 +34,8 @@ type OnUnlink, } from '$lib/utils/actions'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; - import { AssetTypeEnum, AssetVisibility } from '@immich/sdk'; + import { AssetVisibility } from '@immich/sdk'; + import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; @@ -52,8 +53,8 @@ const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; const isLivePhotoCandidate = selectedAssets.length === 2 && - selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) && - selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video); + selectedAssets.some((asset) => asset.isImage) && + selectedAssets.some((asset) => asset.isVideo); return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate); }); diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 813683244e..8c8036903f 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -25,7 +25,7 @@ import { AppRoute, QueryParameter } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { Viewport } from '$lib/stores/assets-store.svelte'; + import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte'; import { lang, locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { preferences } from '$lib/stores/user.store'; @@ -34,9 +34,9 @@ import { parseUtcDate } from '$lib/utils/date-time'; import { handleError } from '$lib/utils/handle-error'; import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; + import { toTimelineAsset } from '$lib/utils/timeline-util'; import { type AlbumResponseDto, - type AssetResponseDto, getPerson, getTagById, type MetadataSearchDto, @@ -59,7 +59,7 @@ let nextPage = $state(1); let searchResultAlbums: AlbumResponseDto[] = $state([]); - let searchResultAssets: AssetResponseDto[] = $state([]); + let searchResultAssets: TimelineAsset[] = $state([]); let isLoading = $state(true); let scrollY = $state(0); let scrollYHistory = 0; @@ -123,7 +123,7 @@ const onAssetDelete = (assetIds: string[]) => { const assetIdSet = new Set(assetIds); - searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); + searchResultAssets = searchResultAssets.filter((asset: TimelineAsset) => !assetIdSet.has(asset.id)); }; const handleSelectAll = () => { assetInteraction.selectAssets(searchResultAssets); @@ -161,7 +161,7 @@ : await searchAssets({ metadataSearchDto: searchDto }); searchResultAlbums.push(...albums.items); - searchResultAssets.push(...assets.items); + searchResultAssets.push(...assets.items.map((asset) => toTimelineAsset(asset))); nextPage = Number(assets.nextPage) || 0; } catch (error) { @@ -239,7 +239,7 @@ if (terms.isNotInAlbum.toString() == 'true') { const assetIdSet = new Set(assetIds); - searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); + searchResultAssets = searchResultAssets.filter((asset) => !assetIdSet.has(asset.id)); } }; @@ -250,30 +250,81 @@ +
+ {#if assetInteraction.selectionActive} +
+ cancelMultiselect(assetInteraction)} + > + + + + + + + { + for (const assetId of assetIds) { + const asset = searchResultAssets.find((searchAsset) => searchAsset.id === assetId); + if (asset) { + asset.isFavorite = isFavorite; + } + } + }} + /> + + + + + + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} + + {/if} + +
+ +
+
+
+ {:else} +
+ goto(previousRoute)} backIcon={mdiArrowLeft}> +
+
+ +
+
+
+ {/if} +
+ {#if terms}
- {#each getObjectKeys(terms) as key (key)} - {@const value = terms[key]} + {#each getObjectKeys(terms) as searchKey (searchKey)} + {@const value = terms[searchKey]}
- {getHumanReadableSearchKey(key as keyof SearchTerms)} + {getHumanReadableSearchKey(searchKey as keyof SearchTerms)}
{#if value !== true}
- {#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'} + {#if (searchKey === 'takenAfter' || searchKey === 'takenBefore') && typeof value === 'string'} {getHumanReadableDate(value)} - {:else if key === 'personIds' && Array.isArray(value)} + {:else if searchKey === 'personIds' && Array.isArray(value)} {#await getPersonName(value) then personName} {personName} {/await} - {:else if key === 'tagIds' && Array.isArray(value)} + {:else if searchKey === 'tagIds' && Array.isArray(value)} {#await getTagNames(value) then tagNames} {tagNames} {/await} diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index b727286590..bdffecc8bc 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -1,3 +1,4 @@ +import type { TimelineAsset } from '$lib/stores/assets-store.svelte'; import { faker } from '@faker-js/faker'; import { AssetTypeEnum, Visibility, type AssetResponseDto } from '@immich/sdk'; import { Sync } from 'factory.ts'; @@ -26,3 +27,25 @@ export const assetFactory = Sync.makeFactory({ hasMetadata: Sync.each(() => faker.datatype.boolean()), visibility: Visibility.Timeline, }); + +export const timelineAssetFactory = Sync.makeFactory({ + id: Sync.each(() => faker.string.uuid()), + ratio: Sync.each(() => faker.number.int()), + ownerId: Sync.each(() => faker.string.uuid()), + thumbhash: Sync.each(() => faker.string.alphanumeric(28)), + localDateTime: Sync.each(() => faker.date.past().toISOString()), + isFavorite: Sync.each(() => faker.datatype.boolean()), + visibility: Visibility.Timeline, + isTrashed: false, + isImage: true, + isVideo: false, + duration: '0:00:00.00000', + stack: null, + projectionType: null, + livePhotoVideoId: Sync.each(() => faker.string.uuid()), + text: Sync.each(() => ({ + city: faker.location.city(), + country: faker.location.country(), + people: [faker.person.fullName()], + })), +});