mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 17:25:11 +03:00
refactor(web): asset interaction (#14662)
* refactor(web): asset interaction * feedback
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { derived, readonly, writable } from 'svelte/store';
|
||||
|
||||
export type AssetInteractionStore = ReturnType<typeof createAssetInteractionStore>;
|
||||
|
||||
export function createAssetInteractionStore() {
|
||||
const selectedAssets = writable(new Set<AssetResponseDto>());
|
||||
const selectedGroup = writable(new Set<string>());
|
||||
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<AssetResponseDto>());
|
||||
// The beginning of the selection range
|
||||
const assetSelectionStart = writable<AssetResponseDto | null>(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),
|
||||
};
|
||||
}
|
||||
40
web/src/lib/stores/asset-interaction.svelte.spec.ts
Normal file
40
web/src/lib/stores/asset-interaction.svelte.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
66
web/src/lib/stores/asset-interaction.svelte.ts
Normal file
66
web/src/lib/stores/asset-interaction.svelte.ts
Normal file
@@ -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<AssetResponseDto>();
|
||||
readonly selectedGroup = new SvelteSet<string>();
|
||||
assetSelectionCandidates = $state(new SvelteSet<AssetResponseDto>());
|
||||
assetSelectionStart = $state<AssetResponseDto | null>(null);
|
||||
|
||||
selectionActive = $derived(this.selectedAssets.size > 0);
|
||||
selectedAssetsArray = $derived([...this.selectedAssets]);
|
||||
|
||||
private user = fromStore<UserAdminResponseDto | undefined>(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user