fix: regression in select-all (#16969)

* bug: select-all

* set->[] in interaction store, clear select-all on cancel

* feedback

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Min Idzelis 2025-03-19 11:55:50 -04:00 committed by GitHub
parent 1a0a9ef36c
commit 9398b0d4b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 82 additions and 82 deletions

View File

@ -52,7 +52,7 @@
{#if isShowConfirmation} {#if isShowConfirmation}
<DeleteAssetDialog <DeleteAssetDialog
size={getOwnedAssets().size} size={getOwnedAssets().length}
onConfirm={handleDelete} onConfirm={handleDelete}
onCancel={() => (isShowConfirmation = false)} onCancel={() => (isShowConfirmation = false)}
/> />

View File

@ -28,7 +28,7 @@
await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }); await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) });
}; };
let menuItemIcon = $derived(getAssets().size === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline); let menuItemIcon = $derived(getAssets().length === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline);
</script> </script>
<svelte:window use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: handleDownloadFiles }} /> <svelte:window use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: handleDownloadFiles }} />

View File

@ -23,7 +23,7 @@
const removeFromAlbum = async () => { const removeFromAlbum = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
prompt: $t('remove_assets_album_confirmation', { values: { count: getAssets().size } }), prompt: $t('remove_assets_album_confirmation', { values: { count: getAssets().length } }),
}); });
if (!isConfirmed) { if (!isConfirmed) {

View File

@ -20,7 +20,7 @@
const handleRemove = async () => { const handleRemove = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
title: $t('remove_assets_title'), title: $t('remove_assets_title'),
prompt: $t('remove_assets_shared_link_confirmation', { values: { count: getAssets().size } }), prompt: $t('remove_assets_shared_link_confirmation', { values: { count: getAssets().length } }),
confirmText: $t('remove'), confirmText: $t('remove'),
}); });

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { AssetBucket } from '$lib/stores/assets-store.svelte'; import { AssetBucket, assetSnapshot, assetsSnapshot } from '$lib/stores/assets-store.svelte';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { getDateLocaleString } from '$lib/utils/timeline-util'; import { getDateLocaleString } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk'; import type { AssetResponseDto } from '@immich/sdk';
@ -71,9 +71,7 @@
assetInteraction.removeGroupFromMultiselectGroup(groupTitle); assetInteraction.removeGroupFromMultiselectGroup(groupTitle);
} }
}; };
const snapshotAssetArray = (assets: AssetResponseDto[]) => {
return assets.map((a) => $state.snapshot(a));
};
const assetMouseEventHandler = (groupTitle: string, asset: AssetResponseDto | null) => { const assetMouseEventHandler = (groupTitle: string, asset: AssetResponseDto | null) => {
// Show multi select icon on hover on date group // Show multi select icon on hover on date group
hoveredDateGroup = groupTitle; hoveredDateGroup = groupTitle;
@ -121,8 +119,8 @@
<div <div
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }} transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
class="inline-block px-2 hover:cursor-pointer" class="inline-block px-2 hover:cursor-pointer"
onclick={() => handleSelectGroup(dateGroup.groupTitle, snapshotAssetArray(dateGroup.getAssets()))} onclick={() => handleSelectGroup(dateGroup.groupTitle, assetsSnapshot(dateGroup.getAssets()))}
onkeydown={() => handleSelectGroup(dateGroup.groupTitle, snapshotAssetArray(dateGroup.getAssets()))} onkeydown={() => handleSelectGroup(dateGroup.groupTitle, assetsSnapshot(dateGroup.getAssets()))}
> >
{#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)} {#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)}
<Icon path={mdiCheckCircle} size="24" color="#4250af" /> <Icon path={mdiCheckCircle} size="24" color="#4250af" />
@ -160,10 +158,10 @@
{showArchiveIcon} {showArchiveIcon}
{asset} {asset}
{groupIndex} {groupIndex}
focussed={assetInteraction.isFocussedAsset(asset)} focussed={assetInteraction.isFocussedAsset(asset.id)}
onClick={(asset) => onClick(dateGroup.getAssets(), dateGroup.groupTitle, asset)} onClick={(asset) => onClick(dateGroup.getAssets(), dateGroup.groupTitle, asset)}
onSelect={(asset) => assetSelectHandler(asset, dateGroup.getAssets(), dateGroup.groupTitle)} onSelect={(asset) => assetSelectHandler(asset, dateGroup.getAssets(), dateGroup.groupTitle)}
onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, $state.snapshot(asset))} onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, assetSnapshot(asset))}
selected={assetInteraction.hasSelectedAsset(asset.id) || dateGroup.bucket.store.albumAssets.has(asset.id)} selected={assetInteraction.hasSelectedAsset(asset.id) || dateGroup.bucket.store.albumAssets.has(asset.id)}
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
handleFocus={() => assetOnFocusHandler(asset)} handleFocus={() => assetOnFocusHandler(asset)}

View File

@ -4,7 +4,7 @@
import type { Action } from '$lib/components/asset-viewer/actions/action'; import type { Action } from '$lib/components/asset-viewer/actions/action';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetBucket, AssetStore } from '$lib/stores/assets-store.svelte'; import { AssetBucket, assetsSnapshot, AssetStore } from '$lib/stores/assets-store.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store'; import { showDeleteModal } from '$lib/stores/preferences.store';
import { isSearchEnabled } from '$lib/stores/search.store'; import { isSearchEnabled } from '$lib/stores/search.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
@ -286,7 +286,7 @@
}; };
const onDelete = () => { const onDelete = () => {
const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
isShowDeleteConfirmation = true; isShowDeleteConfirmation = true;
@ -304,7 +304,7 @@
}; };
const onStackAssets = async () => { const onStackAssets = async () => {
const ids = await stackAssets(assetInteraction.selectedAssetsArray); const ids = await stackAssets(assetInteraction.selectedAssets);
if (ids) { if (ids) {
assetStore.removeAssets(ids); assetStore.removeAssets(ids);
onEscape(); onEscape();
@ -312,8 +312,8 @@
}; };
const toggleArchive = async () => { const toggleArchive = async () => {
await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); await archiveAssets(assetInteraction.selectedAssets, !assetInteraction.isAllArchived);
assetStore.updateAssets(assetInteraction.selectedAssetsArray); assetStore.updateAssets(assetInteraction.selectedAssets);
deselectAllAssets(); deselectAllAssets();
}; };
@ -450,7 +450,7 @@
if (assetInteraction.selectedGroup.has(group)) { if (assetInteraction.selectedGroup.has(group)) {
assetInteraction.removeGroupFromMultiselectGroup(group); assetInteraction.removeGroupFromMultiselectGroup(group);
for (const asset of assets) { for (const asset of assets) {
assetInteraction.removeAssetFromMultiselectGroup(asset); assetInteraction.removeAssetFromMultiselectGroup(asset.id);
} }
} else { } else {
assetInteraction.addGroupToMultiselectGroup(group); assetInteraction.addGroupToMultiselectGroup(group);
@ -471,15 +471,15 @@
return; return;
} }
const rangeSelection = assetInteraction.assetSelectionCandidates.size > 0; const rangeSelection = assetInteraction.assetSelectionCandidates.length > 0;
const deselect = assetInteraction.hasSelectedAsset(asset.id); const deselect = assetInteraction.hasSelectedAsset(asset.id);
// Select/deselect already loaded assets // Select/deselect already loaded assets
if (deselect) { if (deselect) {
for (const candidate of assetInteraction.assetSelectionCandidates) { for (const candidate of assetInteraction.assetSelectionCandidates) {
assetInteraction.removeAssetFromMultiselectGroup(candidate); assetInteraction.removeAssetFromMultiselectGroup(candidate.id);
} }
assetInteraction.removeAssetFromMultiselectGroup(asset); assetInteraction.removeAssetFromMultiselectGroup(asset.id);
} else { } else {
for (const candidate of assetInteraction.assetSelectionCandidates) { for (const candidate of assetInteraction.assetSelectionCandidates) {
handleSelectAsset(candidate); handleSelectAsset(candidate);
@ -510,7 +510,7 @@
await assetStore.loadBucket(bucket.bucketDate); await assetStore.loadBucket(bucket.bucketDate);
for (const asset of bucket.getAssets()) { for (const asset of bucket.getAssets()) {
if (deselect) { if (deselect) {
assetInteraction.removeAssetFromMultiselectGroup(asset); assetInteraction.removeAssetFromMultiselectGroup(asset.id);
} else { } else {
handleSelectAsset(asset); handleSelectAsset(asset);
} }
@ -553,7 +553,7 @@
return; return;
} }
const assets = assetStore.getAssets(); const assets = assetsSnapshot(assetStore.getAssets());
let start = assets.findIndex((a) => a.id === startAsset.id); let start = assets.findIndex((a) => a.id === startAsset.id);
let end = assets.findIndex((a) => a.id === endAsset.id); let end = assets.findIndex((a) => a.id === endAsset.id);
@ -602,7 +602,7 @@
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
let isEmpty = $derived(assetStore.isInitialized && assetStore.buckets.length === 0); let isEmpty = $derived(assetStore.isInitialized && assetStore.buckets.length === 0);
let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
$effect(() => { $effect(() => {
if (isEmpty) { if (isEmpty) {

View File

@ -4,8 +4,8 @@
export interface AssetControlContext { export interface AssetControlContext {
// Wrap assets in a function, because context isn't reactive. // Wrap assets in a function, because context isn't reactive.
getAssets: () => Set<AssetResponseDto>; // All assets includes partners' assets getAssets: () => AssetResponseDto[]; // All assets includes partners' assets
getOwnedAssets: () => Set<AssetResponseDto>; // Only assets owned by the user getOwnedAssets: () => AssetResponseDto[]; // Only assets owned by the user
clearSelect: () => void; clearSelect: () => void;
} }
@ -20,7 +20,7 @@
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
interface Props { interface Props {
assets: Set<AssetResponseDto>; assets: AssetResponseDto[];
clearSelect: () => void; clearSelect: () => void;
ownerId?: string | undefined; ownerId?: string | undefined;
children?: Snippet; children?: Snippet;
@ -30,8 +30,7 @@
setContext({ setContext({
getAssets: () => assets, getAssets: () => assets,
getOwnedAssets: () => getOwnedAssets: () => (ownerId === undefined ? assets : assets.filter((asset) => asset.ownerId === ownerId)),
ownerId === undefined ? assets : new Set([...assets].filter((asset) => asset.ownerId === ownerId)),
clearSelect, clearSelect,
}); });
</script> </script>
@ -39,8 +38,8 @@
<ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md"> <ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
{#snippet leading()} {#snippet leading()}
<div class="font-medium text-immich-primary dark:text-immich-dark-primary"> <div class="font-medium text-immich-primary dark:text-immich-dark-primary">
<p class="block sm:hidden">{assets.size}</p> <p class="block sm:hidden">{assets.length}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p> <p class="hidden sm:block">{$t('selected_count', { values: { count: assets.length } })}</p>
</div> </div>
{/snippet} {/snippet}
{#snippet trailing()} {#snippet trailing()}

View File

@ -15,7 +15,6 @@
import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte'; import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte';
import Map from '$lib/components/shared-components/map/map.svelte'; import Map from '$lib/components/shared-components/map/map.svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
interface Point { interface Point {
lng: number; lng: number;
lat: number; lat: number;

View File

@ -169,9 +169,9 @@
// Select/deselect already loaded assets // Select/deselect already loaded assets
if (deselect) { if (deselect) {
for (const candidate of assetInteraction.assetSelectionCandidates) { for (const candidate of assetInteraction.assetSelectionCandidates) {
assetInteraction.removeAssetFromMultiselectGroup(candidate); assetInteraction.removeAssetFromMultiselectGroup(candidate.id);
} }
assetInteraction.removeAssetFromMultiselectGroup(asset); assetInteraction.removeAssetFromMultiselectGroup(asset.id);
} else { } else {
for (const candidate of assetInteraction.assetSelectionCandidates) { for (const candidate of assetInteraction.assetSelectionCandidates) {
assetInteraction.selectAsset(candidate); assetInteraction.selectAsset(candidate);
@ -217,7 +217,7 @@
}; };
const onDelete = () => { const onDelete = () => {
const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
isShowDeleteConfirmation = true; isShowDeleteConfirmation = true;
@ -245,7 +245,7 @@
}; };
const toggleArchive = async () => { const toggleArchive = async () => {
const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); const ids = await archiveAssets(assetInteraction.selectedAssets, !assetInteraction.isAllArchived);
if (ids) { if (ids) {
assets = assets.filter((asset) => !ids.includes(asset.id)); assets = assets.filter((asset) => !ids.includes(asset.id));
deselectAllAssets(); deselectAllAssets();
@ -407,7 +407,7 @@
}; };
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
$effect(() => { $effect(() => {
if (!lastAssetMouseEvent) { if (!lastAssetMouseEvent) {
@ -438,7 +438,7 @@
{#if isShowDeleteConfirmation} {#if isShowDeleteConfirmation}
<DeleteAssetDialog <DeleteAssetDialog
size={assetInteraction.selectedAssets.size} size={assetInteraction.selectedAssets.length}
onCancel={() => (isShowDeleteConfirmation = false)} onCancel={() => (isShowDeleteConfirmation = false)}
onConfirm={() => handlePromiseError(trashOrDelete(true))} onConfirm={() => handlePromiseError(trashOrDelete(true))}
/> />
@ -480,7 +480,7 @@
{asset} {asset}
selected={assetInteraction.hasSelectedAsset(asset.id)} selected={assetInteraction.hasSelectedAsset(asset.id)}
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
focussed={assetInteraction.isFocussedAsset(asset)} focussed={assetInteraction.isFocussedAsset(asset.id)}
thumbnailWidth={layout.width} thumbnailWidth={layout.width}
thumbnailHeight={layout.height} thumbnailHeight={layout.height}
/> />

View File

@ -4,43 +4,43 @@ import { SvelteSet } from 'svelte/reactivity';
import { fromStore } from 'svelte/store'; import { fromStore } from 'svelte/store';
export class AssetInteraction { export class AssetInteraction {
readonly selectedAssets = new SvelteSet<AssetResponseDto>(); selectedAssets = $state<AssetResponseDto[]>([]);
hasSelectedAsset(assetId: string) { hasSelectedAsset(assetId: string) {
return [...this.selectedAssets.values()].some((asset) => asset.id === assetId); return this.selectedAssets.some((asset) => asset.id === assetId);
} }
readonly selectedGroup = new SvelteSet<string>(); selectedGroup = new SvelteSet<string>();
assetSelectionCandidates = $state(new SvelteSet<AssetResponseDto>()); assetSelectionCandidates = $state<AssetResponseDto[]>([]);
hasSelectionCandidate(assetId: string) { hasSelectionCandidate(assetId: string) {
return [...this.assetSelectionCandidates.values()].some((asset) => asset.id === assetId); return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
} }
assetSelectionStart = $state<AssetResponseDto | null>(null); assetSelectionStart = $state<AssetResponseDto | null>(null);
focussedAssetId = $state<string | null>(null); focussedAssetId = $state<string | null>(null);
selectionActive = $derived(this.selectedAssets.length > 0);
selectionActive = $derived(this.selectedAssets.size > 0);
selectedAssetsArray = $derived([...this.selectedAssets]);
private user = fromStore<UserAdminResponseDto | undefined>(user); private user = fromStore<UserAdminResponseDto | undefined>(user);
private userId = $derived(this.user.current?.id); private userId = $derived(this.user.current?.id);
isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed)); isAllTrashed = $derived(this.selectedAssets.every((asset) => asset.isTrashed));
isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived)); isAllArchived = $derived(this.selectedAssets.every((asset) => asset.isArchived));
isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite)); isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId)); isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
selectAsset(asset: AssetResponseDto) { selectAsset(asset: AssetResponseDto) {
this.selectedAssets.add(asset); if (!this.hasSelectedAsset(asset.id)) {
this.selectedAssets.push(asset);
}
} }
selectAssets(assets: AssetResponseDto[]) { selectAssets(assets: AssetResponseDto[]) {
for (const asset of assets) { for (const asset of assets) {
this.selectedAssets.add(asset); this.selectAsset(asset);
} }
} }
removeAssetFromMultiselectGroup(asset: AssetResponseDto) { removeAssetFromMultiselectGroup(assetId: string) {
const selectedAsset = [...this.selectedAssets.values()].find((a) => a.id === asset.id); const index = this.selectedAssets.findIndex((a) => a.id == assetId);
if (selectedAsset) { if (index !== -1) {
this.selectedAssets.delete(selectedAsset); this.selectedAssets.splice(index, 1);
} }
} }
@ -57,24 +57,24 @@ export class AssetInteraction {
} }
setAssetSelectionCandidates(assets: AssetResponseDto[]) { setAssetSelectionCandidates(assets: AssetResponseDto[]) {
this.assetSelectionCandidates = new SvelteSet(assets); this.assetSelectionCandidates = assets;
} }
clearAssetSelectionCandidates() { clearAssetSelectionCandidates() {
this.assetSelectionCandidates.clear(); this.assetSelectionCandidates = [];
} }
clearMultiselect() { clearMultiselect() {
// Multi-selection // Multi-selection
this.selectedAssets.clear(); this.selectedAssets = [];
this.selectedGroup.clear(); this.selectedGroup.clear();
// Range selection // Range selection
this.assetSelectionCandidates.clear(); this.assetSelectionCandidates = [];
this.assetSelectionStart = null; this.assetSelectionStart = null;
} }
isFocussedAsset(asset: AssetResponseDto) { isFocussedAsset(assetId: string) {
return this.focussedAssetId === asset.id; return this.focussedAssetId === assetId;
} }
} }

View File

@ -57,6 +57,13 @@ function updateObject(target: any, source: any): boolean {
return updated; return updated;
} }
export function assetSnapshot(asset: AssetResponseDto) {
return $state.snapshot(asset);
}
export function assetsSnapshot(assets: AssetResponseDto[]) {
return assets.map((a) => $state.snapshot(a));
}
class IntersectingAsset { class IntersectingAsset {
// --- public --- // --- public ---
readonly #group: AssetDateGroup; readonly #group: AssetDateGroup;
@ -284,9 +291,11 @@ export class AssetBucket {
get lastDateGroup() { get lastDateGroup() {
return this.dateGroups.at(-1); return this.dateGroups.at(-1);
} }
getFirstAsset() { getFirstAsset() {
return this.dateGroups[0]?.getFirstAsset(); return this.dateGroups[0]?.getFirstAsset();
} }
getAssets() { getAssets() {
// eslint-disable-next-line unicorn/no-array-reduce // eslint-disable-next-line unicorn/no-array-reduce
return this.dateGroups.reduce( return this.dateGroups.reduce(

View File

@ -5,7 +5,7 @@ import { NotificationType, notificationController } from '$lib/components/shared
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte'; import { assetsSnapshot, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
import { downloadManager } from '$lib/stores/download'; import { downloadManager } from '$lib/stores/download';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { downloadRequest, getKey, withError } from '$lib/utils'; import { downloadRequest, getKey, withError } from '$lib/utils';
@ -367,7 +367,7 @@ export const getAssetType = (type: AssetTypeEnum) => {
} }
}; };
export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserResponseDto | null): string[] => { export const getSelectedAssets = (assets: AssetResponseDto[], user: UserResponseDto | null): string[] => {
const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id); const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length; const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
@ -474,15 +474,10 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteraction:
await assetStore.loadBucket(bucket.bucketDate); await assetStore.loadBucket(bucket.bucketDate);
if (!get(isSelectingAllAssets)) { if (!get(isSelectingAllAssets)) {
assetInteraction.clearMultiselect();
break; // Cancelled break; // Cancelled
} }
assetInteraction.selectAssets(bucket.getAssets().map((a) => $state.snapshot(a))); assetInteraction.selectAssets(assetsSnapshot(bucket.getAssets()));
// We use setTimeout to allow the UI to update. Otherwise, this may
// cause a long delay between the start of 'select all' and the
// effective update of the UI, depending on the number of assets
// to select
await delay(0);
} }
} catch (error) { } catch (error) {
const $t = get(t); const $t = get(t);

View File

@ -249,7 +249,7 @@
album = await getAlbumInfo({ id: album.id, withoutAssets: true }); album = await getAlbumInfo({ id: album.id, withoutAssets: true });
}; };
const handleAddAssets = async () => { const handleAddAssets = async () => {
const assetIds = timelineInteraction.selectedAssetsArray.map((asset) => asset.id); const assetIds = timelineInteraction.selectedAssets.map((asset) => asset.id);
try { try {
const results = await addAssetsToAlbum({ const results = await addAssetsToAlbum({
@ -364,7 +364,7 @@
}; };
const updateThumbnailUsingCurrentSelection = async () => { const updateThumbnailUsingCurrentSelection = async () => {
if (assetInteraction.selectedAssets.size === 1) { if (assetInteraction.selectedAssets.length === 1) {
const [firstAsset] = assetInteraction.selectedAssets; const [firstAsset] = assetInteraction.selectedAssets;
assetInteraction.clearMultiselect(); assetInteraction.clearMultiselect();
await updateThumbnail(firstAsset.id); await updateThumbnail(firstAsset.id);
@ -479,7 +479,7 @@
{#if assetInteraction.isAllUserOwned} {#if assetInteraction.isAllUserOwned}
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
{#if assetInteraction.selectedAssets.size === 1} {#if assetInteraction.selectedAssets.length === 1}
<MenuOption <MenuOption
text={$t('set_as_album_cover')} text={$t('set_as_album_cover')}
icon={mdiImageOutline} icon={mdiImageOutline}
@ -574,7 +574,7 @@
{#if !timelineInteraction.selectionActive} {#if !timelineInteraction.selectionActive}
{$t('add_to_album')} {$t('add_to_album')}
{:else} {:else}
{$t('selected_count', { values: { count: timelineInteraction.selectedAssets.size } })} {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.length } })}
{/if} {/if}
</p> </p>
{/snippet} {/snippet}

View File

@ -149,7 +149,7 @@
}); });
const handleUnmerge = () => { const handleUnmerge = () => {
assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id)); assetStore.removeAssets(assetInteraction.selectedAssets.map((a) => a.id));
assetInteraction.clearMultiselect(); assetInteraction.clearMultiselect();
viewMode = PersonPageViewMode.VIEW_ASSETS; viewMode = PersonPageViewMode.VIEW_ASSETS;
}; };
@ -368,7 +368,7 @@
{#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS}
<UnMergeFaceSelector <UnMergeFaceSelector
assetIds={assetInteraction.selectedAssetsArray.map((a) => a.id)} assetIds={assetInteraction.selectedAssets.map((a) => a.id)}
personAssets={person} personAssets={person}
onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
onConfirm={handleUnmerge} onConfirm={handleUnmerge}

View File

@ -39,7 +39,7 @@
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
let selectedAssets = $derived(assetInteraction.selectedAssetsArray); let selectedAssets = $derived(assetInteraction.selectedAssets);
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack); let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
let isLinkActionAvailable = $derived.by(() => { let isLinkActionAvailable = $derived.by(() => {
const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId;
@ -97,7 +97,7 @@
></FavoriteAction> ></FavoriteAction>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem /> <DownloadAction menuItem />
{#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected} {#if assetInteraction.selectedAssets.length > 1 || isAssetStackSelected}
<StackAction <StackAction
unstack={isAssetStackSelected} unstack={isAssetStackSelected}
onStack={(assetIds) => assetStore.removeAssets(assetIds)} onStack={(assetIds) => assetStore.removeAssets(assetIds)}
@ -107,7 +107,7 @@
{#if isLinkActionAvailable} {#if isLinkActionAvailable}
<LinkLivePhotoAction <LinkLivePhotoAction
menuItem menuItem
unlink={assetInteraction.selectedAssets.size === 1} unlink={assetInteraction.selectedAssets.length === 1}
onLink={handleLink} onLink={handleLink}
onUnlink={handleUnlink} onUnlink={handleUnlink}
/> />

View File

@ -85,7 +85,7 @@
} }
if (assetInteraction.selectionActive) { if (assetInteraction.selectionActive) {
assetInteraction.selectedAssets.clear(); assetInteraction.selectedAssets = [];
return; return;
} }
if (!$preventRaceConditionSearchBar) { if (!$preventRaceConditionSearchBar) {