mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
refactor(web): asset grid stores (#3464)
* Refactor asset grid stores * Iterate over buckets with for..of loop * Rebase on top of main branch changes
This commit is contained in:
parent
13051c1e5a
commit
5f9dfa9493
15 changed files with 330 additions and 265 deletions
|
|
@ -43,13 +43,15 @@
|
|||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import { downloadArchive } from '../../utils/asset-utils';
|
||||
import { isViewingAssetStoreState } from '$lib/stores/asset-interaction.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
|
||||
|
||||
const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
let isShowAssetSelection = false;
|
||||
|
||||
let isShowShareLinkModal = false;
|
||||
|
|
@ -141,7 +143,7 @@
|
|||
});
|
||||
|
||||
const handleKeyboardPress = (event: KeyboardEvent) => {
|
||||
if (!$isViewingAssetStoreState) {
|
||||
if (!$showAssetViewer) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
if (isMultiSelectionMode) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { assetInteractionStore, assetsInAlbumStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
|
|
@ -9,14 +8,20 @@
|
|||
import Button from '../elements/buttons/button.svelte';
|
||||
import AssetGrid from '../photos-page/asset-grid.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import { createAssetStore } from '$lib/stores/assets.store';
|
||||
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const assetStore = createAssetStore();
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { selectedAssets, assetsInAlbumState } = assetInteractionStore;
|
||||
|
||||
export let albumId: string;
|
||||
export let assetsInAlbum: AssetResponseDto[];
|
||||
|
||||
onMount(() => {
|
||||
$assetsInAlbumStoreState = assetsInAlbum;
|
||||
$assetsInAlbumState = assetsInAlbum;
|
||||
});
|
||||
|
||||
const addSelectedAssets = async () => {
|
||||
|
|
@ -64,6 +69,6 @@
|
|||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
<section class="grid h-screen bg-immich-bg pl-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||
<AssetGrid isAlbumSelectionMode={true} />
|
||||
<AssetGrid {assetStore} {assetInteractionStore} isAlbumSelectionMode={true} />
|
||||
</section>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@
|
|||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte';
|
||||
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { isShowDetail } from '$lib/stores/preferences.store';
|
||||
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
|
||||
import NavigationArea from './navigation-area.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import type { AssetStore } from '$lib/stores/assets.store';
|
||||
|
||||
export let assetStore: AssetStore | null = null;
|
||||
export let asset: AssetResponseDto;
|
||||
export let publicSharedKey = '';
|
||||
export let showNavigation = true;
|
||||
|
|
@ -134,7 +135,7 @@
|
|||
|
||||
for (const asset of deletedAssets) {
|
||||
if (asset.status == 'SUCCESS') {
|
||||
assetStore.removeAsset(asset.id);
|
||||
assetStore?.removeAsset(asset.id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -158,7 +159,7 @@
|
|||
});
|
||||
|
||||
asset.isFavorite = data.isFavorite;
|
||||
assetStore.updateAsset(asset.id, data.isFavorite);
|
||||
assetStore?.updateAsset(asset.id, data.isFavorite);
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { get } from 'svelte/store';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import SelectAll from 'svelte-material-icons/SelectAll.svelte';
|
||||
import TimerSand from 'svelte-material-icons/TimerSand.svelte';
|
||||
import { assetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
import { assetGridState, assetStore } from '$lib/stores/assets.store';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
|
||||
import { BucketPosition } from '$lib/models/asset-grid-state';
|
||||
import type { AssetStore } from '$lib/stores/assets.store';
|
||||
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
|
||||
export let assetStore: AssetStore;
|
||||
export let assetInteractionStore: AssetInteractionStore;
|
||||
|
||||
let selecting = false;
|
||||
|
||||
const handleSelectAll = async () => {
|
||||
try {
|
||||
selecting = true;
|
||||
let _assetGridState = new AssetGridState();
|
||||
assetGridState.subscribe((state) => {
|
||||
_assetGridState = state;
|
||||
});
|
||||
|
||||
for (let i = 0; i < _assetGridState.buckets.length; i++) {
|
||||
await assetStore.getAssetsByBucket(_assetGridState.buckets[i].bucketDate, BucketPosition.Unknown);
|
||||
for (const asset of _assetGridState.buckets[i].assets) {
|
||||
const assetGridState = get(assetStore);
|
||||
for (const bucket of assetGridState.buckets) {
|
||||
await assetStore.getAssetsByBucket(bucket.bucketDate, BucketPosition.Unknown);
|
||||
for (const asset of bucket.assets) {
|
||||
assetInteractionStore.addAssetToMultiselectGroup(asset);
|
||||
}
|
||||
}
|
||||
|
||||
selecting = false;
|
||||
} catch (e) {
|
||||
handleError(e, 'Error selecting all assets');
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
assetInteractionStore,
|
||||
assetSelectionCandidates,
|
||||
assetsInAlbumStoreState,
|
||||
isMultiSelectStoreState,
|
||||
selectedAssets,
|
||||
selectedGroup,
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
||||
|
|
@ -19,6 +10,9 @@
|
|||
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import type { AssetStore } from '$lib/stores/assets.store';
|
||||
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
export let bucketDate: string;
|
||||
|
|
@ -26,6 +20,12 @@
|
|||
export let isAlbumSelectionMode = false;
|
||||
export let viewportWidth: number;
|
||||
|
||||
export let assetStore: AssetStore;
|
||||
export let assetInteractionStore: AssetInteractionStore;
|
||||
|
||||
const { selectedGroup, selectedAssets, assetsInAlbumState, assetSelectionCandidates, isMultiSelectState } =
|
||||
assetInteractionStore;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let isMouseOverGroup = false;
|
||||
|
|
@ -94,10 +94,10 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if ($isMultiSelectStoreState) {
|
||||
if ($isMultiSelectState) {
|
||||
assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle);
|
||||
} else {
|
||||
assetInteractionStore.setViewingAsset(asset);
|
||||
assetViewingStore.setAssetId(asset.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
// Show multi select icon on hover on date group
|
||||
hoveredDateGroup = dateGroupTitle;
|
||||
|
||||
if ($isMultiSelectStoreState) {
|
||||
if ($isMultiSelectState) {
|
||||
dispatch('selectAssetCandidates', { asset });
|
||||
}
|
||||
};
|
||||
|
|
@ -207,9 +207,9 @@
|
|||
on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)}
|
||||
on:mouse-event={() => assetMouseEventHandler(dateGroupTitle, asset)}
|
||||
selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.some(({ id }) => id === asset.id)}
|
||||
selected={$selectedAssets.has(asset) || $assetsInAlbumState.some(({ id }) => id === asset.id)}
|
||||
selectionCandidate={$assetSelectionCandidates.has(asset)}
|
||||
disabled={$assetsInAlbumStoreState.some(({ id }) => id === asset.id)}
|
||||
disabled={$assetsInAlbumState.some(({ id }) => id === asset.id)}
|
||||
thumbnailWidth={box.width}
|
||||
thumbnailHeight={box.height}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { BucketPosition } from '$lib/models/asset-grid-state';
|
||||
import {
|
||||
assetInteractionStore,
|
||||
assetSelectionCandidates,
|
||||
assetSelectionStart,
|
||||
isMultiSelectStoreState,
|
||||
isViewingAssetStoreState,
|
||||
selectedAssets,
|
||||
viewingAssetStoreState,
|
||||
} from '$lib/stores/asset-interaction.store';
|
||||
import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
||||
import type { UserResponseDto } from '@api';
|
||||
|
|
@ -31,11 +22,20 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { isSearchEnabled } from '$lib/stores/search.store';
|
||||
import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
|
||||
import type { AssetStore } from '$lib/stores/assets.store';
|
||||
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
|
||||
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
export let isAlbumSelectionMode = false;
|
||||
export let showMemoryLane = false;
|
||||
|
||||
export let assetStore: AssetStore;
|
||||
export let assetInteractionStore: AssetInteractionStore;
|
||||
|
||||
const { assetSelectionCandidates, assetSelectionStart, selectedAssets, isMultiSelectState } = assetInteractionStore;
|
||||
|
||||
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore;
|
||||
|
||||
let viewportHeight = 0;
|
||||
let viewportWidth = 0;
|
||||
let assetGridElement: HTMLElement;
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
// Get asset bucket if bucket height is smaller than viewport height
|
||||
let bucketsToFetchInitially: string[] = [];
|
||||
let initialBucketsHeight = 0;
|
||||
$assetGridState.buckets.every((bucket) => {
|
||||
$assetStore.buckets.every((bucket) => {
|
||||
if (initialBucketsHeight < viewportHeight) {
|
||||
initialBucketsHeight += bucket.bucketHeight;
|
||||
bucketsToFetchInitially.push(bucket.bucketDate);
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (!$isViewingAssetStoreState) {
|
||||
if (!$showAssetViewer) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
assetInteractionStore.clearMultiselect();
|
||||
|
|
@ -121,12 +121,18 @@
|
|||
assetGridElement.scrollBy(0, event.detail.heightDelta);
|
||||
}
|
||||
|
||||
const navigateToPreviousAsset = () => {
|
||||
assetInteractionStore.navigateAsset('previous');
|
||||
const navigateToPreviousAsset = async () => {
|
||||
const prevAsset = await assetStore.getAdjacentAsset($viewingAsset.id, 'previous');
|
||||
if (prevAsset) {
|
||||
assetViewingStore.setAssetId(prevAsset);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToNextAsset = () => {
|
||||
assetInteractionStore.navigateAsset('next');
|
||||
const navigateToNextAsset = async () => {
|
||||
const nextAsset = await assetStore.getAdjacentAsset($viewingAsset.id, 'next');
|
||||
if (nextAsset) {
|
||||
assetViewingStore.setAssetId(nextAsset);
|
||||
}
|
||||
};
|
||||
|
||||
let lastScrollPosition = 0;
|
||||
|
|
@ -228,8 +234,8 @@
|
|||
assetInteractionStore.clearAssetSelectionCandidates();
|
||||
|
||||
if ($assetSelectionStart && rangeSelection) {
|
||||
let startBucketIndex = $assetGridState.loadedAssets[$assetSelectionStart.id];
|
||||
let endBucketIndex = $assetGridState.loadedAssets[asset.id];
|
||||
let startBucketIndex = $assetStore.loadedAssets[$assetSelectionStart.id];
|
||||
let endBucketIndex = $assetStore.loadedAssets[asset.id];
|
||||
|
||||
if (endBucketIndex < startBucketIndex) {
|
||||
[startBucketIndex, endBucketIndex] = [endBucketIndex, startBucketIndex];
|
||||
|
|
@ -237,7 +243,7 @@
|
|||
|
||||
// Select/deselect assets in all intermediate buckets
|
||||
for (let bucketIndex = startBucketIndex + 1; bucketIndex < endBucketIndex; bucketIndex++) {
|
||||
const bucket = $assetGridState.buckets[bucketIndex];
|
||||
const bucket = $assetStore.buckets[bucketIndex];
|
||||
await assetStore.getAssetsByBucket(bucket.bucketDate, BucketPosition.Unknown);
|
||||
for (const asset of bucket.assets) {
|
||||
if (deselect) {
|
||||
|
|
@ -250,7 +256,7 @@
|
|||
|
||||
// Update date group selection
|
||||
for (let bucketIndex = startBucketIndex; bucketIndex <= endBucketIndex; bucketIndex++) {
|
||||
const bucket = $assetGridState.buckets[bucketIndex];
|
||||
const bucket = $assetStore.buckets[bucketIndex];
|
||||
|
||||
// Split bucket into date groups and check each group
|
||||
const assetsGroupByDate = splitBucketIntoDateGroups(bucket.assets, $locale);
|
||||
|
|
@ -279,18 +285,18 @@
|
|||
return;
|
||||
}
|
||||
|
||||
let start = $assetGridState.assets.indexOf(rangeStart);
|
||||
let end = $assetGridState.assets.indexOf(asset);
|
||||
let start = $assetStore.assets.indexOf(rangeStart);
|
||||
let end = $assetStore.assets.indexOf(asset);
|
||||
|
||||
if (start > end) {
|
||||
[start, end] = [end, start];
|
||||
}
|
||||
|
||||
assetInteractionStore.setAssetSelectionCandidates($assetGridState.assets.slice(start, end + 1));
|
||||
assetInteractionStore.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1));
|
||||
};
|
||||
|
||||
const onSelectStart = (e: Event) => {
|
||||
if ($isMultiSelectStoreState && shiftKeyIsDown) {
|
||||
if ($isMultiSelectState && shiftKeyIsDown) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
|
@ -302,8 +308,9 @@
|
|||
<ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} />
|
||||
{/if}
|
||||
|
||||
{#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
|
||||
{#if bucketInfo && viewportHeight && $assetStore.timelineHeight > viewportHeight}
|
||||
<Scrollbar
|
||||
{assetStore}
|
||||
scrollbarHeight={viewportHeight}
|
||||
scrollTop={lastScrollPosition}
|
||||
on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)}
|
||||
|
|
@ -324,15 +331,12 @@
|
|||
{#if showMemoryLane}
|
||||
<MemoryLane />
|
||||
{/if}
|
||||
<section id="virtual-timeline" style:height={$assetGridState.timelineHeight + 'px'}>
|
||||
{#each $assetGridState.buckets as bucket, bucketIndex (bucketIndex)}
|
||||
<section id="virtual-timeline" style:height={$assetStore.timelineHeight + 'px'}>
|
||||
{#each $assetStore.buckets as bucket, bucketIndex (bucketIndex)}
|
||||
<IntersectionObserver
|
||||
on:intersected={intersectedHandler}
|
||||
on:hidden={async () => {
|
||||
// If bucket is hidden and in loading state, cancel the request
|
||||
if ($loadingBucketState[bucket.bucketDate]) {
|
||||
await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate);
|
||||
}
|
||||
await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate);
|
||||
}}
|
||||
let:intersecting
|
||||
top={750}
|
||||
|
|
@ -342,6 +346,8 @@
|
|||
<div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}>
|
||||
{#if intersecting}
|
||||
<AssetDateGroup
|
||||
{assetStore}
|
||||
{assetInteractionStore}
|
||||
{isAlbumSelectionMode}
|
||||
on:shift={handleScrollTimeline}
|
||||
on:selectAssetCandidates={handleSelectAssetCandidates}
|
||||
|
|
@ -360,13 +366,14 @@
|
|||
</section>
|
||||
|
||||
<Portal target="body">
|
||||
{#if $isViewingAssetStoreState}
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
asset={$viewingAssetStoreState}
|
||||
{assetStore}
|
||||
asset={$viewingAsset}
|
||||
on:navigate-previous={navigateToPreviousAsset}
|
||||
on:navigate-next={navigateToNextAsset}
|
||||
on:close={() => {
|
||||
assetInteractionStore.setIsViewingAsset(false);
|
||||
assetViewingStore.showAssetViewer(false);
|
||||
}}
|
||||
on:archived={handleArchiveSuccess}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import { flip } from 'svelte/animate';
|
||||
import { archivedAsset } from '$lib/stores/archived-asset.store';
|
||||
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
||||
import { isViewingAssetStoreState } from '$lib/stores/asset-interaction.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
export let viewFrom: ViewFrom;
|
||||
export let showArchiveIcon = false;
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
let selectedAsset: AssetResponseDto;
|
||||
let currentViewAssetIndex = 0;
|
||||
|
||||
|
|
@ -33,7 +35,7 @@
|
|||
|
||||
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
||||
selectedAsset = assets[currentViewAssetIndex];
|
||||
$isViewingAssetStoreState = true;
|
||||
$showAssetViewer = true;
|
||||
pushState(selectedAsset.id);
|
||||
};
|
||||
|
||||
|
|
@ -81,7 +83,7 @@
|
|||
};
|
||||
|
||||
const closeViewer = () => {
|
||||
$isViewingAssetStoreState = false;
|
||||
$showAssetViewer = false;
|
||||
history.pushState(null, '', `${$page.url.pathname}`);
|
||||
};
|
||||
|
||||
|
|
@ -117,7 +119,7 @@
|
|||
{/if}
|
||||
|
||||
<!-- Overlay Asset Viewer -->
|
||||
{#if $isViewingAssetStoreState}
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
asset={selectedAsset}
|
||||
publicSharedKey={sharedLink?.key}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@
|
|||
<script lang="ts">
|
||||
import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
|
||||
|
||||
import { assetGridState } from '$lib/stores/assets.store';
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { SegmentScrollbarLayout } from './segment-scrollbar-layout';
|
||||
import type { AssetStore } from '$lib/stores/assets.store';
|
||||
|
||||
export let scrollTop = 0;
|
||||
export let scrollbarHeight = 0;
|
||||
export let assetStore: AssetStore;
|
||||
|
||||
$: timelineHeight = $assetGridState.timelineHeight;
|
||||
$: timelineHeight = $assetStore.timelineHeight;
|
||||
$: timelineScrolltop = (scrollbarPosition / scrollbarHeight) * timelineHeight;
|
||||
|
||||
let segmentScrollbarLayout: SegmentScrollbarLayout[] = [];
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
$: {
|
||||
let result: SegmentScrollbarLayout[] = [];
|
||||
for (const bucket of $assetGridState.buckets) {
|
||||
for (const bucket of $assetStore.buckets) {
|
||||
let segmentLayout = new SegmentScrollbarLayout();
|
||||
segmentLayout.count = bucket.assets.length;
|
||||
segmentLayout.height = (bucket.bucketHeight / timelineHeight) * scrollbarHeight;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue