From bccf2f60b2caa072e0bd7ab6f88b8d65a774a290 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 13 Dec 2024 10:59:14 -0600 Subject: [PATCH 01/24] fix(web): upload info panel covers timeline navigation bar (#14651) --- web/src/lib/components/shared-components/upload-panel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index 2381b5a423f81..0eb7d1655c7d9 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -48,7 +48,7 @@ } uploadAssetsStore.reset(); }} - class="fixed bottom-6 right-6 z-[10000]" + class="fixed bottom-6 right-16 z-[10000]" > {#if showDetail}
Date: Fri, 13 Dec 2024 18:13:38 +0100 Subject: [PATCH 02/24] fix(server): fixed email footer image stretched #14617 (#14671) --- server/src/emails/components/footer.template.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/emails/components/footer.template.tsx b/server/src/emails/components/footer.template.tsx index 7c41a7196d18d..c84246bf87e23 100644 --- a/server/src/emails/components/footer.template.tsx +++ b/server/src/emails/components/footer.template.tsx @@ -5,12 +5,14 @@ export const ImmichFooter = () => ( <> - - - +
+ + + +
-
+
Immich From b5022d80d6cdb4a27e33970522868d6ea58e3744 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:30:33 +0100 Subject: [PATCH 03/24] refactor(web): asset interaction (#14662) * refactor(web): asset interaction * feedback --- .../components/album-page/album-viewer.svelte | 19 ++-- .../memory-page/memory-viewer.svelte | 29 +++---- .../actions/select-all-assets.svelte | 10 +-- .../photos-page/asset-date-group.svelte | 26 +++--- .../components/photos-page/asset-grid.svelte | 77 ++++++++--------- .../individual-shared-viewer.svelte | 17 ++-- .../gallery-viewer/gallery-viewer.svelte | 62 +++++++------ web/src/lib/stores/asset-interaction.store.ts | 86 ------------------- .../stores/asset-interaction.svelte.spec.ts | 40 +++++++++ .../lib/stores/asset-interaction.svelte.ts | 66 ++++++++++++++ web/src/lib/utils/asset-utils.ts | 10 +-- .../[[assetId=id]]/+page.svelte | 77 +++++++++-------- .../[[assetId=id]]/+page.svelte | 22 ++--- .../[[assetId=id]]/+page.svelte | 26 +++--- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 12 ++- .../[[assetId=id]]/+page.svelte | 44 +++++----- .../(user)/photos/[[assetId=id]]/+page.svelte | 52 +++++------ .../[[assetId=id]]/+page.svelte | 33 ++++--- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 22 ++--- 21 files changed, 375 insertions(+), 367 deletions(-) delete mode 100644 web/src/lib/stores/asset-interaction.store.ts create mode 100644 web/src/lib/stores/asset-interaction.svelte.spec.ts create mode 100644 web/src/lib/stores/asset-interaction.svelte.ts diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 1dc43c5b61188..02544e3e07e78 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -4,7 +4,6 @@ import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; @@ -20,6 +19,7 @@ import AlbumSummary from './album-summary.svelte'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { sharedLink: SharedLinkResponseDto; @@ -34,8 +34,7 @@ let { isViewing: showAssetViewer } = assetViewingStore; const assetStore = new AssetStore({ albumId: album.id, order: album.order }); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); dragAndDropFilesStore.subscribe((value) => { if (value.isDragging && value.files.length > 0) { @@ -52,8 +51,8 @@ use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: () => { - if (!$showAssetViewer && $isMultiSelectState) { - cancelMultiselect(assetInteractionStore); + if (!$showAssetViewer && assetInteraction.selectionActive) { + cancelMultiselect(assetInteraction); } }, }} @@ -61,13 +60,13 @@ />
- {#if $isMultiSelectState} + {#if assetInteraction.selectionActive} assetInteractionStore.clearMultiselect()} + assets={assetInteraction.selectedAssets} + clearSelect={() => assetInteraction.clearMultiselect()} > - + {#if sharedLink.allowDownload} {/if} @@ -102,7 +101,7 @@
- +

(0, { duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), }); @@ -130,7 +129,7 @@ const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]); const handleEscape = async () => goto(AppRoute.PHOTOS); - const handleSelectAll = () => assetInteractionStore.selectAssets(current?.memory.assets || []); + const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []); const handleAction = async (action: 'reset' | 'pause' | 'play') => { switch (action) { case 'play': { @@ -212,10 +211,6 @@ current = loadFromParams($memories, target); }); - let isMultiSelectionMode = $derived($selectedAssets.size > 0); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); - $effect(() => { handlePromiseError(handleProgress($progressBarController)); }); @@ -223,7 +218,6 @@ $effect(() => { handlePromiseError(handleAction(galleryInView ? 'pause' : 'play')); }); - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); -{#if isMultiSelectionMode} +{#if assetInteraction.selectionActive}
- cancelMultiselect(assetInteractionStore)}> + cancelMultiselect(assetInteraction)} + > @@ -249,14 +246,14 @@ - + - - {#if $preferences.tags.enabled && isAllUserOwned} + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} @@ -490,7 +487,7 @@ onPrevious={handlePreviousAsset} assets={current.memory.assets} {viewport} - {assetInteractionStore} + {assetInteraction} />

diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index cc27f3ebbeb5b..9e7c2b916304b 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -1,24 +1,24 @@ diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index b2780cc1a06b0..586491ef47dc0 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -2,7 +2,6 @@ import { intersectionObserver } from '$lib/actions/intersection-observer'; import Icon from '$lib/components/elements/icon.svelte'; import Skeleton from '$lib/components/photos-page/skeleton.svelte'; - import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store'; import { navigate } from '$lib/utils/navigation'; import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util'; @@ -13,6 +12,7 @@ import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; import { TUNABLES } from '$lib/utils/tunables'; import { generateId } from '$lib/utils/generate-id'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; export let element: HTMLElement | undefined = undefined; export let isSelectionMode = false; @@ -25,7 +25,7 @@ export let renderThumbsAtTopMargin: string | undefined = undefined; export let assetStore: AssetStore; export let bucket: AssetBucket; - export let assetInteractionStore: AssetInteractionStore; + export let assetInteraction: AssetInteraction; export let onScrollTarget: ScrollTargetListener | undefined = undefined; export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined; @@ -43,13 +43,11 @@ /* TODO figure out a way to calculate this*/ const TITLE_HEIGHT = 51; - const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore; - let isMouseOverGroup = false; let hoveredDateGroup = ''; const onClick = (assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => { - if (isSelectionMode || $isMultiSelectState) { + if (isSelectionMode || assetInteraction.selectionActive) { assetSelectHandler(asset, assets, groupTitle); return; } @@ -69,13 +67,15 @@ onSelectAssets(asset); // Check if all assets are selected in a group to toggle the group selection's icon - let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length; + let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => + assetInteraction.selectedAssets.has(asset), + ).length; // if all assets are selected in a group, add the group to selected group if (selectedAssetsInGroupCount == assetsInDateGroup.length) { - assetInteractionStore.addGroupToMultiselectGroup(groupTitle); + assetInteraction.addGroupToMultiselectGroup(groupTitle); } else { - assetInteractionStore.removeGroupFromMultiselectGroup(groupTitle); + assetInteraction.removeGroupFromMultiselectGroup(groupTitle); } }; @@ -83,7 +83,7 @@ // Show multi select icon on hover on date group hoveredDateGroup = groupTitle; - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { onSelectAssetCandidates(asset); } }; @@ -151,14 +151,14 @@ class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" style:width={dateGroup.geometry.containerWidth + 'px'} > - {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle))} + {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} > - {#if $selectedGroup.has(dateGroup.groupTitle)} + {#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)} {:else} @@ -212,8 +212,8 @@ onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)} onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)} onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)} - selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} - selectionCandidate={$assetSelectionCandidates.has(asset)} + selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} + selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)} disabled={$assetStore.albumAssets.has(asset.id)} thumbnailWidth={box.width} thumbnailHeight={box.height} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 5055cdcf4b979..cc64c6f02b334 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -3,7 +3,6 @@ import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; import type { Action } from '$lib/components/asset-viewer/actions/action'; import { AppRoute, AssetAction } from '$lib/constants'; - import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store'; import { locale, showDeleteModal } from '$lib/stores/preferences.store'; @@ -37,6 +36,7 @@ import type { UpdatePayload } from 'vite'; import { generateId } from '$lib/utils/generate-id'; import { isTimelineScrolling } from '$lib/stores/timeline.store'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { isSelectionMode?: boolean; @@ -46,7 +46,7 @@ additionally, update the page location/url with the asset as the asset-grid is scrolled */ enableRouting: boolean; assetStore: AssetStore; - assetInteractionStore: AssetInteractionStore; + assetInteraction: AssetInteraction; removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null; withStacked?: boolean; showArchiveIcon?: boolean; @@ -64,7 +64,7 @@ singleSelect = false, enableRouting, assetStore = $bindable(), - assetInteractionStore, + assetInteraction, removeAction = null, withStacked = false, showArchiveIcon = false, @@ -78,8 +78,6 @@ }: Props = $props(); let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore; - const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = - assetInteractionStore; const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); @@ -437,11 +435,11 @@ (assetIds) => $assetStore.removeAssets(assetIds), idsSelectedAssets, ); - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); }; const onDelete = () => { - const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed); + const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { isShowDeleteConfirmation = true; @@ -459,7 +457,7 @@ }; const onStackAssets = async () => { - const ids = await stackAssets(Array.from($selectedAssets)); + const ids = await stackAssets(assetInteraction.selectedAssetsArray); if (ids) { $assetStore.removeAssets(ids); onEscape(); @@ -467,7 +465,7 @@ }; const toggleArchive = async () => { - const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived); + const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); if (ids) { $assetStore.removeAssets(ids); deselectAllAssets(); @@ -482,7 +480,7 @@ const handleSelectAsset = (asset: AssetResponseDto) => { if (!$assetStore.albumAssets.has(asset.id)) { - assetInteractionStore.selectAsset(asset); + assetInteraction.selectAsset(asset); } }; @@ -573,7 +571,7 @@ let shiftKeyIsDown = $state(false); const deselectAllAssets = () => { - cancelMultiselect(assetInteractionStore); + cancelMultiselect(assetInteraction); }; const onKeyDown = (event: KeyboardEvent) => { @@ -606,13 +604,13 @@ }; const handleGroupSelect = (group: string, assets: AssetResponseDto[]) => { - if ($selectedGroup.has(group)) { - assetInteractionStore.removeGroupFromMultiselectGroup(group); + if (assetInteraction.selectedGroup.has(group)) { + assetInteraction.removeGroupFromMultiselectGroup(group); for (const asset of assets) { - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } } else { - assetInteractionStore.addGroupToMultiselectGroup(group); + assetInteraction.addGroupToMultiselectGroup(group); for (const asset of assets) { handleSelectAsset(asset); } @@ -631,26 +629,26 @@ return; } - const rangeSelection = $assetSelectionCandidates.size > 0; - const deselect = $selectedAssets.has(asset); + const rangeSelection = assetInteraction.assetSelectionCandidates.size > 0; + const deselect = assetInteraction.selectedAssets.has(asset); // Select/deselect already loaded assets if (deselect) { - for (const candidate of $assetSelectionCandidates || []) { - assetInteractionStore.removeAssetFromMultiselectGroup(candidate); + for (const candidate of assetInteraction.assetSelectionCandidates) { + assetInteraction.removeAssetFromMultiselectGroup(candidate); } - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } else { - for (const candidate of $assetSelectionCandidates || []) { + for (const candidate of assetInteraction.assetSelectionCandidates) { handleSelectAsset(candidate); } handleSelectAsset(asset); } - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); - if ($assetSelectionStart && rangeSelection) { - let startBucketIndex = $assetStore.getBucketIndexByAssetId($assetSelectionStart.id); + if (assetInteraction.assetSelectionStart && rangeSelection) { + let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id); let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id); if (startBucketIndex === null || endBucketIndex === null) { @@ -667,7 +665,7 @@ await $assetStore.loadBucket(bucket.bucketDate); for (const asset of bucket.assets) { if (deselect) { - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } else { handleSelectAsset(asset); } @@ -682,16 +680,16 @@ const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale); for (const dateGroup of assetsGroupByDate) { const dateGroupTitle = formatGroupTitle(dateGroup.date); - if (dateGroup.assets.every((a) => $selectedAssets.has(a))) { - assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle); + if (dateGroup.assets.every((a) => assetInteraction.selectedAssets.has(a))) { + assetInteraction.addGroupToMultiselectGroup(dateGroupTitle); } else { - assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle); + assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle); } } } } - assetInteractionStore.setAssetSelectionStart(deselect ? null : asset); + assetInteraction.setAssetSelectionStart(deselect ? null : asset); }; const selectAssetCandidates = (endAsset: AssetResponseDto) => { @@ -699,7 +697,7 @@ return; } - const startAsset = $assetSelectionStart; + const startAsset = assetInteraction.assetSelectionStart; if (!startAsset) { return; } @@ -711,11 +709,11 @@ [start, end] = [end, start]; } - assetInteractionStore.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); + assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); }; const onSelectStart = (e: Event) => { - if ($isMultiSelectState && shiftKeyIsDown) { + if (assetInteraction.selectionActive && shiftKeyIsDown) { e.preventDefault(); } }; @@ -724,12 +722,11 @@ }); let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0); - let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id)); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); + let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); $effect(() => { if (isEmpty) { - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); } }); @@ -760,12 +757,12 @@ { shortcut: { key: 'Escape' }, onShortcut: onEscape }, { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, - { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) }, + { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) }, { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement }, { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement }, ]; - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { shortcuts.push( { shortcut: { key: 'Delete' }, onShortcut: onDelete }, { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, @@ -781,13 +778,13 @@ $effect(() => { if (!lastAssetMouseEvent) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); $effect(() => { if (!shiftKeyIsDown) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); @@ -889,7 +886,7 @@ {withStacked} {showArchiveIcon} {assetStore} - {assetInteractionStore} + {assetInteraction} {isSelectionMode} {singleSelect} {onScrollTarget} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 5d625cef9d711..ebc4b49001104 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -15,11 +15,11 @@ import ControlAppBar from '../shared-components/control-app-bar.svelte'; import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; import { cancelMultiselect } from '$lib/utils/asset-utils'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import type { Viewport } from '$lib/stores/assets.store'; import { t } from 'svelte-i18n'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { sharedLink: SharedLinkResponseDto; @@ -29,12 +29,10 @@ let { sharedLink = $bindable(), isOwned }: Props = $props(); const viewport: Viewport = $state({ width: 0, height: 0 }); - const assetInteractionStore = createAssetInteractionStore(); - const { selectedAssets } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); let innerWidth: number = $state(0); let assets = $derived(sharedLink.assets); - let isMultiSelectionMode = $derived($selectedAssets.size > 0); dragAndDropFilesStore.subscribe((value) => { if (value.isDragging && value.files.length > 0) { @@ -73,15 +71,18 @@ }; const handleSelectAll = () => { - assetInteractionStore.selectAssets(assets); + assetInteraction.selectAssets(assets); };
- {#if isMultiSelectionMode} - cancelMultiselect(assetInteractionStore)}> + {#if assetInteraction.selectionActive} + cancelMultiselect(assetInteraction)} + > {#if sharedLink?.allowDownload} @@ -112,6 +113,6 @@ {/if}
- +
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index eda340e7e2c92..8f8a067a902ba 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -5,7 +5,6 @@ import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import type { Viewport } from '$lib/stores/assets.store'; import { showDeleteModal } from '$lib/stores/preferences.store'; import { deleteAssets } from '$lib/utils/actions'; @@ -22,10 +21,11 @@ import Portal from '../portal/portal.svelte'; import { handlePromiseError } from '$lib/utils'; import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte'; + import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { assets: AssetResponseDto[]; - assetInteractionStore: AssetInteractionStore; + assetInteraction: AssetInteraction; disableAssetSelect?: boolean; showArchiveIcon?: boolean; viewport: Viewport; @@ -38,7 +38,7 @@ let { assets = $bindable(), - assetInteractionStore = $bindable(), + assetInteraction, disableAssetSelect = false, showArchiveIcon = false, viewport, @@ -51,11 +51,8 @@ let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; - const { assetSelectionCandidates, assetSelectionStart, selectedAssets, isMultiSelectState } = assetInteractionStore; - let showShortcuts = $state(false); let currentViewAssetIndex = 0; - let isMultiSelectionMode = $derived($selectedAssets.size > 0); let shiftKeyIsDown = $state(false); let lastAssetMouseEvent: AssetResponseDto | null = $state(null); @@ -66,11 +63,11 @@ }; const selectAllAssets = () => { - assetInteractionStore.selectAssets(assets); + assetInteraction.selectAssets(assets); }; const deselectAllAssets = () => { - cancelMultiselect(assetInteractionStore); + cancelMultiselect(assetInteraction); }; const onKeyDown = (event: KeyboardEvent) => { @@ -91,23 +88,23 @@ if (!asset) { return; } - const deselect = $selectedAssets.has(asset); + const deselect = assetInteraction.selectedAssets.has(asset); // Select/deselect already loaded assets if (deselect) { - for (const candidate of $assetSelectionCandidates || []) { - assetInteractionStore.removeAssetFromMultiselectGroup(candidate); + for (const candidate of assetInteraction.assetSelectionCandidates) { + assetInteraction.removeAssetFromMultiselectGroup(candidate); } - assetInteractionStore.removeAssetFromMultiselectGroup(asset); + assetInteraction.removeAssetFromMultiselectGroup(asset); } else { - for (const candidate of $assetSelectionCandidates || []) { - assetInteractionStore.selectAsset(candidate); + for (const candidate of assetInteraction.assetSelectionCandidates) { + assetInteraction.selectAsset(candidate); } - assetInteractionStore.selectAsset(asset); + assetInteraction.selectAsset(asset); } - assetInteractionStore.clearAssetSelectionCandidates(); - assetInteractionStore.setAssetSelectionStart(deselect ? null : asset); + assetInteraction.clearAssetSelectionCandidates(); + assetInteraction.setAssetSelectionStart(deselect ? null : asset); }; const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => { @@ -122,7 +119,7 @@ return; } - const startAsset = $assetSelectionStart; + const startAsset = assetInteraction.assetSelectionStart; if (!startAsset) { return; } @@ -134,17 +131,17 @@ [start, end] = [end, start]; } - assetInteractionStore.setAssetSelectionCandidates(assets.slice(start, end + 1)); + assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1)); }; const onSelectStart = (e: Event) => { - if ($isMultiSelectState && shiftKeyIsDown) { + if (assetInteraction.selectionActive && shiftKeyIsDown) { e.preventDefault(); } }; const onDelete = () => { - const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed); + const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { isShowDeleteConfirmation = true; @@ -168,11 +165,11 @@ (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))), idsSelectedAssets, ); - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); }; const toggleArchive = async () => { - const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived); + const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); if (ids) { assets.filter((asset) => !ids.includes(asset.id)); deselectAllAssets(); @@ -191,7 +188,7 @@ { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() }, ]; - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { shortcuts.push( { shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets }, { shortcut: { key: 'Delete' }, onShortcut: onDelete }, @@ -266,14 +263,13 @@ }; const assetMouseEventHandler = (asset: AssetResponseDto | null) => { - if ($isMultiSelectState) { + if (assetInteraction.selectionActive) { handleSelectAssetCandidates(asset); } }; let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); - let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id)); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); + let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); let geometry = $derived( (() => { @@ -297,13 +293,13 @@ $effect(() => { if (!lastAssetMouseEvent) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); $effect(() => { if (!shiftKeyIsDown) { - assetInteractionStore.clearAssetSelectionCandidates(); + assetInteraction.clearAssetSelectionCandidates(); } }); @@ -318,7 +314,7 @@ {#if isShowDeleteConfirmation} (isShowDeleteConfirmation = false)} onConfirm={() => handlePromiseError(trashOrDelete(true))} /> @@ -340,7 +336,7 @@ { - if (isMultiSelectionMode) { + if (assetInteraction.selectionActive) { handleSelectAssets(asset); return; } @@ -351,8 +347,8 @@ onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)} {showArchiveIcon} {asset} - selected={$selectedAssets.has(asset)} - selectionCandidate={$assetSelectionCandidates.has(asset)} + selected={assetInteraction.selectedAssets.has(asset)} + selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)} thumbnailWidth={geometry.boxes[i].width} thumbnailHeight={geometry.boxes[i].height} /> diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts deleted file mode 100644 index f7db5382b02f9..0000000000000 --- a/web/src/lib/stores/asset-interaction.store.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { AssetResponseDto } from '@immich/sdk'; -import { derived, readonly, writable } from 'svelte/store'; - -export type AssetInteractionStore = ReturnType; - -export function createAssetInteractionStore() { - const selectedAssets = writable(new Set()); - const selectedGroup = writable(new Set()); - const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); - - // Candidates for the range selection. This set includes only loaded assets, so it improves highlight - // performance. From the user's perspective, range is highlighted almost immediately - const assetSelectionCandidates = writable(new Set()); - // The beginning of the selection range - const assetSelectionStart = writable(null); - - const selectAsset = (asset: AssetResponseDto) => { - selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset)); - }; - - const selectAssets = (assets: AssetResponseDto[]) => { - selectedAssets.update(($selectedAssets) => { - for (const asset of assets) { - $selectedAssets.add(asset); - } - return $selectedAssets; - }); - }; - - const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => { - selectedAssets.update(($selectedAssets) => { - $selectedAssets.delete(asset); - return $selectedAssets; - }); - }; - - const addGroupToMultiselectGroup = (group: string) => { - selectedGroup.update(($selectedGroup) => $selectedGroup.add(group)); - }; - - const removeGroupFromMultiselectGroup = (group: string) => { - selectedGroup.update(($selectedGroup) => { - $selectedGroup.delete(group); - return $selectedGroup; - }); - }; - - const setAssetSelectionStart = (asset: AssetResponseDto | null) => { - assetSelectionStart.set(asset); - }; - - const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => { - assetSelectionCandidates.set(new Set(assets)); - }; - - const clearAssetSelectionCandidates = () => { - assetSelectionCandidates.set(new Set()); - }; - - const clearMultiselect = () => { - // Multi-selection - selectedAssets.set(new Set()); - selectedGroup.set(new Set()); - - // Range selection - assetSelectionCandidates.set(new Set()); - assetSelectionStart.set(null); - }; - - return { - selectAsset, - selectAssets, - removeAssetFromMultiselectGroup, - addGroupToMultiselectGroup, - removeGroupFromMultiselectGroup, - setAssetSelectionCandidates, - clearAssetSelectionCandidates, - setAssetSelectionStart, - clearMultiselect, - isMultiSelectState: readonly(isMultiSelectStoreState), - selectedAssets: readonly(selectedAssets), - selectedGroup: readonly(selectedGroup), - assetSelectionCandidates: readonly(assetSelectionCandidates), - assetSelectionStart: readonly(assetSelectionStart), - }; -} diff --git a/web/src/lib/stores/asset-interaction.svelte.spec.ts b/web/src/lib/stores/asset-interaction.svelte.spec.ts new file mode 100644 index 0000000000000..5d3043b37c3c2 --- /dev/null +++ b/web/src/lib/stores/asset-interaction.svelte.spec.ts @@ -0,0 +1,40 @@ +import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; +import { resetSavedUser, user } from '$lib/stores/user.store'; +import { assetFactory } from '@test-data/factories/asset-factory'; +import { userAdminFactory } from '@test-data/factories/user-factory'; + +describe('AssetInteraction', () => { + let assetInteraction: AssetInteraction; + + beforeEach(() => { + assetInteraction = new AssetInteraction(); + }); + + it('calculates derived values from selection', () => { + assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true })); + assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false })); + + expect(assetInteraction.selectionActive).toBe(true); + expect(assetInteraction.isAllTrashed).toBe(false); + expect(assetInteraction.isAllArchived).toBe(false); + expect(assetInteraction.isAllFavorite).toBe(true); + }); + + it('updates isAllUserOwned when the active user changes', () => { + const [user1, user2] = userAdminFactory.buildList(2); + assetInteraction.selectAsset(assetFactory.build({ ownerId: user1.id })); + + const cleanup = $effect.root(() => { + expect(assetInteraction.isAllUserOwned).toBe(false); + + user.set(user1); + expect(assetInteraction.isAllUserOwned).toBe(true); + + user.set(user2); + expect(assetInteraction.isAllUserOwned).toBe(false); + }); + + cleanup(); + resetSavedUser(); + }); +}); diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts new file mode 100644 index 0000000000000..4397c7f71f2ae --- /dev/null +++ b/web/src/lib/stores/asset-interaction.svelte.ts @@ -0,0 +1,66 @@ +import { user } from '$lib/stores/user.store'; +import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk'; +import { SvelteSet } from 'svelte/reactivity'; +import { fromStore } from 'svelte/store'; + +export class AssetInteraction { + readonly selectedAssets = new SvelteSet(); + readonly selectedGroup = new SvelteSet(); + assetSelectionCandidates = $state(new SvelteSet()); + assetSelectionStart = $state(null); + + selectionActive = $derived(this.selectedAssets.size > 0); + selectedAssetsArray = $derived([...this.selectedAssets]); + + private user = fromStore(user); + private userId = $derived(this.user.current?.id); + + isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed)); + isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived)); + isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite)); + isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId)); + + selectAsset(asset: AssetResponseDto) { + this.selectedAssets.add(asset); + } + + selectAssets(assets: AssetResponseDto[]) { + for (const asset of assets) { + this.selectedAssets.add(asset); + } + } + + removeAssetFromMultiselectGroup(asset: AssetResponseDto) { + this.selectedAssets.delete(asset); + } + + addGroupToMultiselectGroup(group: string) { + this.selectedGroup.add(group); + } + + removeGroupFromMultiselectGroup(group: string) { + this.selectedGroup.delete(group); + } + + setAssetSelectionStart(asset: AssetResponseDto | null) { + this.assetSelectionStart = asset; + } + + setAssetSelectionCandidates(assets: AssetResponseDto[]) { + this.assetSelectionCandidates = new SvelteSet(assets); + } + + clearAssetSelectionCandidates() { + this.assetSelectionCandidates.clear(); + } + + clearMultiselect() { + // Multi-selection + this.selectedAssets.clear(); + this.selectedGroup.clear(); + + // Range selection + this.assetSelectionCandidates.clear(); + this.assetSelectionStart = null; + } +} diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 37041ecbc43a4..5b06a66597f17 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte'; import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; -import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; +import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { downloadManager } from '$lib/stores/download'; @@ -460,7 +460,7 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S } }; -export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { +export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: AssetInteraction) => { if (get(isSelectingAllAssets)) { // Selection is already ongoing return; @@ -474,7 +474,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt if (!get(isSelectingAllAssets)) { break; // Cancelled } - assetInteractionStore.selectAssets(bucket.assets); + assetInteraction.selectAssets(bucket.assets); // We use setTimeout to allow the UI to update. Otherwise, this may // cause a long delay between the start of 'select all' and the @@ -489,9 +489,9 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt } }; -export const cancelMultiselect = (assetInteractionStore: AssetInteractionStore) => { +export const cancelMultiselect = (assetInteraction: AssetInteraction) => { isSelectingAllAssets.set(false); - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); }; export const toggleArchive = async (asset: AssetResponseDto) => { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5c63d8e1a3d24..0f6c62a5fafaf 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -35,7 +35,6 @@ import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import { AppRoute, AlbumPageViewMode } from '$lib/constants'; import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; @@ -87,6 +86,7 @@ import { onDestroy } from 'svelte'; import { confirmAlbumDelete } from '$lib/utils/album-utils'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -107,11 +107,8 @@ let reactions: ActivityResponseDto[] = $state([]); let albumOrder: AssetOrder | undefined = $state(data.album.order); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; - - const timelineInteractionStore = createAssetInteractionStore(); - const { selectedAssets: timelineSelected } = timelineInteractionStore; + const assetInteraction = new AssetInteraction(); + const timelineInteraction = new AssetInteraction(); afterNavigate(({ from }) => { let url: string | undefined = from?.url?.pathname; @@ -234,8 +231,8 @@ if ($showAssetViewer) { return; } - if ($isMultiSelectState) { - cancelMultiselect(assetInteractionStore); + if (assetInteraction.selectionActive) { + cancelMultiselect(assetInteraction); return; } await goto(backUrl); @@ -245,9 +242,8 @@ const refreshAlbum = async () => { album = await getAlbumInfo({ id: album.id, withoutAssets: true }); }; - const handleAddAssets = async () => { - const assetIds = [...$timelineSelected].map((asset) => asset.id); + const assetIds = timelineInteraction.selectedAssetsArray.map((asset) => asset.id); try { const results = await addAssetsToAlbum({ @@ -263,7 +259,7 @@ await refreshAlbum(); - timelineInteractionStore.clearMultiselect(); + timelineInteraction.clearMultiselect(); await setModeToView(); } catch (error) { handleError(error, $t('errors.error_adding_assets_to_album')); @@ -284,13 +280,13 @@ }; const handleCloseSelectAssets = async () => { - timelineInteractionStore.clearMultiselect(); + timelineInteraction.clearMultiselect(); await setModeToView(); }; const handleSelectFromComputer = async () => { await openFileUploadDialog({ albumId: album.id }); - timelineInteractionStore.clearMultiselect(); + timelineInteraction.clearMultiselect(); await setModeToView(); }; @@ -359,16 +355,16 @@ } viewMode = AlbumPageViewMode.VIEW; - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); await updateThumbnail(assetId); }; const updateThumbnailUsingCurrentSelection = async () => { - if ($selectedAssets.size === 1) { - const assetId = [...$selectedAssets][0].id; - assetInteractionStore.clearMultiselect(); - await updateThumbnail(assetId); + if (assetInteraction.selectedAssets.size === 1) { + const [firstAsset] = assetInteraction.selectedAssets; + assetInteraction.clearMultiselect(); + await updateThumbnail(firstAsset.id); } }; @@ -410,9 +406,6 @@ let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId)); let isOwned = $derived($user.id == album.ownerId); - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); let showActivityStatus = $derived( album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0), @@ -433,40 +426,50 @@
- {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> + {#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > - + - {#if isAllUserOwned} - assetStore.triggerUpdate()} /> + {#if assetInteraction.isAllUserOwned} + assetStore.triggerUpdate()} + /> {/if} - {#if isAllUserOwned} + {#if assetInteraction.isAllUserOwned} - {#if $selectedAssets.size === 1} + {#if assetInteraction.selectedAssets.size === 1} updateThumbnailUsingCurrentSelection()} /> {/if} - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} + /> {/if} - {#if $preferences.tags.enabled && isAllUserOwned} + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} - {#if isOwned || isAllUserOwned} + {#if isOwned || assetInteraction.isAllUserOwned} {/if} - {#if isAllUserOwned} + {#if assetInteraction.isAllUserOwned} {/if} @@ -540,10 +543,10 @@ {#snippet leading()}

- {#if $timelineSelected.size === 0} + {#if !timelineInteraction.selectionActive} {$t('add_to_album')} {:else} - {$t('selected_count', { values: { count: $timelineSelected.size } })} + {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.size } })} {/if}

{/snippet} @@ -556,7 +559,7 @@ > {$t('select_from_computer')} - {/snippet} @@ -579,7 +582,7 @@ {:else} @@ -587,7 +590,7 @@ enableRouting={true} {album} {assetStore} - {assetInteractionStore} + {assetInteraction} isShared={album.albumUsers.length > 0} isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3402dff9600ca..5301364ccb116 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -12,12 +12,12 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import type { PageData } from './$types'; import { mdiPlus, mdiDotsVertical } from '@mdi/js'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -26,26 +26,26 @@ let { data }: Props = $props(); const assetStore = new AssetStore({ isArchived: true }); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; - - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); + const assetInteraction = new AssetInteraction(); onDestroy(() => { assetStore.destroy(); }); -{#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > assetStore.removeAssets(assetIds)} /> - + - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} /> assetStore.removeAssets(assetIds)} /> @@ -53,8 +53,8 @@ {/if} - - + + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6635eda6e98de..33a03292cd9bf 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -14,7 +14,6 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import type { PageData } from './$types'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; @@ -22,6 +21,7 @@ import { onDestroy } from 'svelte'; import { preferences } from '$lib/stores/user.store'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -30,10 +30,7 @@ let { data }: Props = $props(); const assetStore = new AssetStore({ isFavorite: true }); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; - - let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived)); + const assetInteraction = new AssetInteraction(); onDestroy(() => { assetStore.destroy(); @@ -41,11 +38,14 @@ -{#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > assetStore.removeAssets(assetIds)} /> - + @@ -54,7 +54,11 @@ - assetStore.removeAssets(assetIds)} /> + assetStore.removeAssets(assetIds)} + /> {#if $preferences.tags.enabled} {/if} @@ -63,8 +67,8 @@ {/if} - - + + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 065b28c674274..5119905652dd7 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -3,7 +3,6 @@ import { page } from '$app/stores'; import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; @@ -17,6 +16,7 @@ import type { PageData } from './$types'; import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -31,7 +31,7 @@ let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree)); - const assetInteractionStore = createAssetInteractionStore(); + const assetInteraction = new AssetInteraction(); onMount(async () => { await foldersStore.fetchUniquePaths(); @@ -80,7 +80,7 @@
{ - assetInteractionStore.clearMultiselect(); assetStore.destroy(); });
- {#if $isMultiSelectState} - + {#if assetInteraction.selectionActive} + @@ -50,5 +48,5 @@ {/snippet} {/if} - +
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 143a19dd5cb03..6788c678ede4c 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 @@ -27,7 +27,6 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { websocketEvents } from '$lib/stores/websocket'; @@ -58,8 +57,9 @@ import { listNavigation } from '$lib/actions/list-navigation'; import { t } from 'svelte-i18n'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; - import { preferences, user } from '$lib/stores/user.store'; + import { preferences } from '$lib/stores/user.store'; import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -78,8 +78,7 @@ handlePromiseError(assetStore.updateOptions(assetStoreOptions)); }); - const assetInteractionStore = createAssetInteractionStore(); - const { selectedAssets, isMultiSelectState } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS); let isEditingName = $state(false); @@ -123,8 +122,8 @@ if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) { return; } - if ($isMultiSelectState) { - assetInteractionStore.clearMultiselect(); + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); return; } else { await goto(previousRoute); @@ -149,8 +148,8 @@ }); const handleUnmerge = () => { - $assetStore.removeAssets([...$selectedAssets].map((a) => a.id)); - assetInteractionStore.clearMultiselect(); + $assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id)); + assetInteraction.clearMultiselect(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -194,7 +193,7 @@ handleError(error, $t('errors.unable_to_set_feature_photo')); } - assetInteractionStore.clearMultiselect(); + assetInteraction.clearMultiselect(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -336,15 +335,11 @@ handlePromiseError(updateAssetCount()); } }); - - let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} a.id)} + assetIds={assetInteraction.selectedAssetsArray.map((a) => a.id)} personAssets={person} onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} onConfirm={handleUnmerge} @@ -375,15 +370,18 @@ {/if}
- {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> + {#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > - + - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} /> - $assetStore.removeAssets(assetIds)} /> - {#if $preferences.tags.enabled && isAllUserOwned} + $assetStore.removeAssets(assetIds)} + /> + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} $assetStore.removeAssets(assetIds)} /> @@ -453,7 +455,7 @@ { - const selection = [...$selectedAssets]; - isAllOwned = selection.every((asset) => asset.ownerId === $user.id); - isAllFavorite = selection.every((asset) => asset.isFavorite); - isAssetStackSelected = selection.length === 1 && !!selection[0].stack; - const isLivePhoto = selection.length === 1 && !!selection[0].livePhotoVideoId; + let selectedAssets = $derived(assetInteraction.selectedAssetsArray); + let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack); + let isLinkActionAvailable = $derived.by(() => { + const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; const isLivePhotoCandidate = - selection.length === 2 && - selection.some((asset) => asset.type === AssetTypeEnum.Image) && - selection.some((asset) => asset.type === AssetTypeEnum.Video); - isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate); - }); + selectedAssets.length === 2 && + selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) && + selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video); + return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate); + }); const handleEscape = () => { if ($showAssetViewer) { return; } - if ($isMultiSelectState) { - assetInteractionStore.clearMultiselect(); + if (assetInteraction.selectionActive) { + assetInteraction.clearMultiselect(); return; } }; @@ -78,22 +70,22 @@ }); -{#if $isMultiSelectState} +{#if assetInteraction.selectionActive} assetInteractionStore.clearMultiselect()} + assets={assetInteraction.selectedAssets} + clearSelect={() => assetInteraction.clearMultiselect()} > - + - assetStore.triggerUpdate()} /> + assetStore.triggerUpdate()} /> - {#if $selectedAssets.size > 1 || isAssetStackSelected} + {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected} assetStore.removeAssets(assetIds)} @@ -103,7 +95,7 @@ {#if isLinkActionAvailable} @@ -121,11 +113,11 @@ {/if} - + ; - - let isMultiSelectionMode = $derived($selectedAssets.size > 0); - let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); - let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); let searchQuery = $derived($page.url.searchParams.get(QueryParameter.QUERY)); onMount(() => { @@ -86,8 +81,8 @@ return; } - if (isMultiSelectionMode) { - $selectedAssets = new Set(); + if (assetInteraction.selectionActive) { + assetInteraction.selectedAssets.clear(); return; } if (!$preventRaceConditionSearchBar) { @@ -131,7 +126,7 @@ searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); }; const handleSelectAll = () => { - assetInteractionStore.selectAssets(searchResultAssets); + assetInteraction.selectAssets(searchResultAssets); }; async function onSearchQueryUpdate() { @@ -231,29 +226,31 @@ function getObjectKeys(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]; } - let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id));
- {#if isMultiSelectionMode} + {#if assetInteraction.selectionActive}
- cancelMultiselect(assetInteractionStore)}> + cancelMultiselect(assetInteraction)} + > - + - - {#if $preferences.tags.enabled && isAllUserOwned} + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} {/if} @@ -333,7 +330,7 @@ {#if searchResultAssets.length > 0} { return Object.fromEntries(tags.map((tag) => [tag.value, tag])); @@ -198,7 +198,7 @@
{#if tag} - + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8803ea38c826a..7f97d3772b973 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -15,7 +15,6 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; - import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; @@ -26,6 +25,7 @@ import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; interface Props { data: PageData; @@ -39,8 +39,7 @@ const options = { isTrashed: true }; const assetStore = new AssetStore(options); - const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; + const assetInteraction = new AssetInteraction(); const handleEmptyTrash = async () => { const isConfirmed = await dialogController.show({ @@ -93,25 +92,28 @@ }); -{#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> - +{#if assetInteraction.selectionActive} + assetInteraction.clearMultiselect()} + > + assetStore.removeAssets(assetIds)} /> assetStore.removeAssets(assetIds)} /> {/if} {#if $featureFlags.loaded && $featureFlags.trash} - + {#snippet buttons()}
- +
{$t('restore_all')}
- handleEmptyTrash()} disabled={$isMultiSelectState}> + handleEmptyTrash()} disabled={assetInteraction.selectionActive}>
{$t('empty_trash')} @@ -120,7 +122,7 @@
{/snippet} - +

{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}

From cc111a1fcb1987ecba8dc025a1177de0f8f394cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 13:43:31 -0600 Subject: [PATCH 04/24] fix(deps): update dependency analyzer to v7 (#14673) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- mobile/immich_lint/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 9d1a3c26b3ca8..5d871b03e6b46 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -5,7 +5,7 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - analyzer: ^6.8.0 + analyzer: ^7.0.0 analyzer_plugin: ^0.11.3 custom_lint_builder: ^0.6.4 glob: ^2.1.2 From dd9feeec45d272dbe2c2100e40e0f465d960a964 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 14 Dec 2024 12:53:15 -0700 Subject: [PATCH 05/24] chore(mobile): remove screen auto-dimming (#14699) --- .../pages/backup/backup_controller.page.dart | 183 +++++++----------- 1 file changed, 65 insertions(+), 118 deletions(-) diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index d8baecf808d63..6783f7b54a178 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -1,11 +1,9 @@ -import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -31,8 +29,6 @@ class BackupControllerPage extends HookConsumerWidget { BackUpState backupState = ref.watch(backupProvider); final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; final didGetBackupInfo = useState(false); - final isScreenDarkened = useState(false); - final darkenScreenTimer = useRef(null); bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; @@ -43,25 +39,6 @@ class BackupControllerPage extends HookConsumerWidget { ? false : true; - void startScreenDarkenTimer() { - darkenScreenTimer.value = Timer(const Duration(seconds: 30), () { - isScreenDarkened.value = true; - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - }); - } - - void stopScreenDarkenTimer() { - darkenScreenTimer.value?.cancel(); - isScreenDarkened.value = false; - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: [ - SystemUiOverlay.top, - SystemUiOverlay.bottom, - ], - ); - } - useEffect( () { // Update the background settings information just to make sure we @@ -77,8 +54,6 @@ class BackupControllerPage extends HookConsumerWidget { return () { WakelockPlus.disable(); - darkenScreenTimer.value?.cancel(); - isScreenDarkened.value = false; }; }, [], @@ -99,10 +74,8 @@ class BackupControllerPage extends HookConsumerWidget { useEffect( () { if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - startScreenDarkenTimer(); WakelockPlus.enable(); } else { - stopScreenDarkenTimer(); WakelockPlus.disable(); } @@ -297,103 +270,77 @@ class BackupControllerPage extends HookConsumerWidget { ); } - return GestureDetector( - onTap: () { - if (isScreenDarkened.value) { - stopScreenDarkenTimer(); - } - if (backupState.backupProgress == BackUpProgressEnum.inProgress) { - startScreenDarkenTimer(); - } - }, - child: AnimatedOpacity( - opacity: isScreenDarkened.value ? 0.1 : 1.0, - duration: const Duration(seconds: 1), - child: Scaffold( - appBar: AppBar( - elevation: 0, - title: const Text( - "backup_controller_page_backup", - ).tr(), - leading: IconButton( - onPressed: () { - ref.watch(websocketProvider.notifier).listenUploadEvent(); - context.maybePop(true); - }, + return Scaffold( + appBar: AppBar( + elevation: 0, + title: const Text( + "backup_controller_page_backup", + ).tr(), + leading: IconButton( + onPressed: () { + ref.watch(websocketProvider.notifier).listenUploadEvent(); + context.maybePop(true); + }, + splashRadius: 24, + icon: const Icon( + Icons.arrow_back_ios_rounded, + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: IconButton( + onPressed: () => context.pushRoute(const BackupOptionsRoute()), splashRadius: 24, icon: const Icon( - Icons.arrow_back_ios_rounded, + Icons.settings_outlined, ), ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - onPressed: () => - context.pushRoute(const BackupOptionsRoute()), - splashRadius: 24, - icon: const Icon( - Icons.settings_outlined, - ), - ), - ), - ], ), - body: Stack( - children: [ - Padding( - padding: - const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), - child: ListView( - // crossAxisAlignment: CrossAxisAlignment.start, - children: hasAnyAlbum - ? [ - buildFolderSelectionTile(), - BackupInfoCard( - title: "backup_controller_page_total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref - .watch(backupProvider) - .availableAlbums - .isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref - .watch(backupProvider) - .availableAlbums - .isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: - "backup_controller_page_remainder_sub".tr(), - info: ref - .watch(backupProvider) - .availableAlbums - .isEmpty - ? "..." - : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", - ), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ] - : [ - buildFolderSelectionTile(), - if (!didGetBackupInfo.value) buildLoadingIndicator(), - ], - ), - ), - ], + ], + ), + body: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), + child: ListView( + // crossAxisAlignment: CrossAxisAlignment.start, + children: hasAnyAlbum + ? [ + buildFolderSelectionTile(), + BackupInfoCard( + title: "backup_controller_page_total".tr(), + subtitle: "backup_controller_page_total_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${backupState.allUniqueAssets.length}", + ), + BackupInfoCard( + title: "backup_controller_page_backup".tr(), + subtitle: "backup_controller_page_backup_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${backupState.selectedAlbumsBackupAssetsIds.length}", + ), + BackupInfoCard( + title: "backup_controller_page_remainder".tr(), + subtitle: "backup_controller_page_remainder_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", + ), + const Divider(), + const CurrentUploadingAssetInfoBox(), + if (!hasExclusiveAccess) buildBackgroundBackupInfo(), + buildBackupButton(), + ] + : [ + buildFolderSelectionTile(), + if (!didGetBackupInfo.value) buildLoadingIndicator(), + ], + ), ), - ), + ], ), ); } From fe554c3a5bb0139d874ccd34cc947c7628543e5b Mon Sep 17 00:00:00 2001 From: Alex Sherwin Date: Sun, 15 Dec 2024 16:09:52 -0500 Subject: [PATCH 06/24] fix(mobile): set custom headers on external url (#14707) (#14708) --- mobile/lib/services/auth.service.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 0393470098109..08741a15db0f6 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -57,13 +57,18 @@ class AuthService { Future validateAuxilaryServerUrl(String url) async { final httpclient = HttpClient(); - final accessToken = _authRepository.getAccessToken(); bool isValid = false; try { final uri = Uri.parse('$url/users/me'); final request = await httpclient.getUrl(uri); - request.headers.add('x-immich-user-token', accessToken); + + // add auth token + any configured custom headers + final customHeaders = ApiService.getRequestHeaders(); + customHeaders.forEach((key, value) { + request.headers.add(key, value); + }); + final response = await request.close(); if (response.statusCode == 200) { isValid = true; From 6b0f9ec46cb5340932add131949c5952395aec7d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Dec 2024 08:42:40 -0600 Subject: [PATCH 07/24] chore(mobile): post release tasks (#14656) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 49ac6c4cffbc9..613a8fdf1079d 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -403,7 +403,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 184; + CURRENT_PROJECT_VERSION = 185; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 184; + CURRENT_PROJECT_VERSION = 185; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -575,7 +575,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 184; + CURRENT_PROJECT_VERSION = 185; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 28d21e266e2a2..2a74f884851a0 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.122.2 + 1.122.3 CFBundleSignature ???? CFBundleVersion - 184 + 185 FLTEnableImpeller ITSAppUsesNonExemptEncryption From 8945a5d862254bae244a52fa0edf04be5daf596c Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:45:01 +0100 Subject: [PATCH 08/24] fix: reduce the number of API requests when changing route (#14666) * fix: reduce the number of API requests when changing route * fix: reset `userInteraction` after sign out --- .../components/forms/create-user-form.svelte | 6 +++-- .../components/forms/edit-user-form.svelte | 5 ++-- .../navigation-bar/navigation-bar.svelte | 9 ++++--- .../side-bar/recent-albums.svelte | 6 +++++ .../side-bar/server-status.svelte | 9 ++++++- .../side-bar/storage-space.svelte | 13 ++++++---- web/src/lib/stores/server-info.store.ts | 4 --- web/src/lib/stores/user.svelte.ts | 26 +++++++++++++++++++ web/src/lib/utils/auth.ts | 5 ++-- 9 files changed, 63 insertions(+), 20 deletions(-) delete mode 100644 web/src/lib/stores/server-info.store.ts create mode 100644 web/src/lib/stores/user.svelte.ts diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index b1599a24b2099..7aa1c76ed3216 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -1,7 +1,7 @@ -{#if shouldShowHelpPanel && aboutInfo} - (shouldShowHelpPanel = false)} info={aboutInfo} /> +{#if shouldShowHelpPanel && info} + (shouldShowHelpPanel = false)} {info} /> {/if}
diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte index d90d7dec01c87..b11935d643a1b 100644 --- a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte +++ b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte @@ -4,13 +4,19 @@ import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; import { handleError } from '$lib/utils/handle-error'; import { t } from 'svelte-i18n'; + import { userInteraction } from '$lib/stores/user.svelte'; let albums: AlbumResponseDto[] = $state([]); onMount(async () => { + if (userInteraction.recentAlbums) { + albums = userInteraction.recentAlbums; + return; + } try { const allAlbums = await getAllAlbums({}); albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); + userInteraction.recentAlbums = albums; } catch (error) { handleError(error, $t('failed_to_load_assets')); } diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte index 2a0e6a082140f..e1d7340c46bb6 100644 --- a/web/src/lib/components/shared-components/side-bar/server-status.svelte +++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte @@ -12,17 +12,24 @@ } from '@immich/sdk'; import Icon from '$lib/components/elements/icon.svelte'; import { mdiAlert } from '@mdi/js'; + import { userInteraction } from '$lib/stores/user.svelte'; const { serverVersion, connected } = websocketStore; let isOpen = $state(false); - let info: ServerAboutResponseDto | undefined = $state(); let versions: ServerVersionHistoryResponseDto[] = $state([]); onMount(async () => { + if (userInteraction.aboutInfo && userInteraction.versions && $serverVersion) { + info = userInteraction.aboutInfo; + versions = userInteraction.versions; + return; + } await requestServerInfo(); [info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]); + userInteraction.aboutInfo = info; + userInteraction.versions = versions; }); let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich'); let version = $derived( diff --git a/web/src/lib/components/shared-components/side-bar/storage-space.svelte b/web/src/lib/components/shared-components/side-bar/storage-space.svelte index c0de9378acaff..9472397565d6a 100644 --- a/web/src/lib/components/shared-components/side-bar/storage-space.svelte +++ b/web/src/lib/components/shared-components/side-bar/storage-space.svelte @@ -1,18 +1,18 @@ @@ -54,7 +57,7 @@