1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +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:
Sergey Kondrikov 2023-08-01 04:27:56 +03:00 committed by GitHub
parent 13051c1e5a
commit 5f9dfa9493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 330 additions and 265 deletions

View file

@ -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) {

View file

@ -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>

View file

@ -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,

View file

@ -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');

View file

@ -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}
/>

View file

@ -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}
/>

View file

@ -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}

View file

@ -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;

View file

@ -1,49 +1,68 @@
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
import { api, AssetResponseDto } from '@api';
import { derived, writable } from 'svelte/store';
import { assetGridState, assetStore } from './assets.store';
import type { AssetResponseDto } from '../../api/open-api';
// Asset Viewer
export const viewingAssetStoreState = writable<AssetResponseDto>();
export const isViewingAssetStoreState = writable<boolean>(false);
export interface AssetInteractionStore {
addAssetToMultiselectGroup: (asset: AssetResponseDto) => void;
removeAssetFromMultiselectGroup: (asset: AssetResponseDto) => void;
addGroupToMultiselectGroup: (group: string) => void;
removeGroupFromMultiselectGroup: (group: string) => void;
setAssetSelectionCandidates: (assets: AssetResponseDto[]) => void;
clearAssetSelectionCandidates: () => void;
setAssetSelectionStart: (asset: AssetResponseDto | null) => void;
clearMultiselect: () => void;
isMultiSelectState: {
subscribe: (run: (value: boolean) => void, invalidate?: (value?: boolean) => void) => () => void;
};
assetsInAlbumState: {
subscribe: (
run: (value: AssetResponseDto[]) => void,
invalidate?: (value?: AssetResponseDto[]) => void,
) => () => void;
set: (value: AssetResponseDto[]) => void;
};
selectedAssets: {
subscribe: (
run: (value: Set<AssetResponseDto>) => void,
invalidate?: (value?: Set<AssetResponseDto>) => void,
) => () => void;
};
selectedGroup: {
subscribe: (run: (value: Set<string>) => void, invalidate?: (value?: Set<string>) => void) => () => void;
};
assetSelectionCandidates: {
subscribe: (
run: (value: Set<AssetResponseDto>) => void,
invalidate?: (value?: Set<AssetResponseDto>) => void,
) => () => void;
};
assetSelectionStart: {
subscribe: (
run: (value: AssetResponseDto | null) => void,
invalidate?: (value?: AssetResponseDto | null) => void,
) => () => void;
};
}
/**
* Multi-selection mode
*/
export const assetsInAlbumStoreState = writable<AssetResponseDto[]>([]);
// Selected assets
export const selectedAssets = writable<Set<AssetResponseDto>>(new Set());
// Selected date groups
export const selectedGroup = writable<Set<string>>(new Set());
// If any asset selected
export const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
/**
* Range selection
*/
// 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
export const assetSelectionCandidates = writable<Set<AssetResponseDto>>(new Set());
// The beginning of the selection range
export const assetSelectionStart = writable<AssetResponseDto | null>(null);
function createAssetInteractionStore() {
let _assetGridState = new AssetGridState();
let _viewingAssetStoreState: AssetResponseDto;
export function createAssetInteractionStore(): AssetInteractionStore {
let _selectedAssets: Set<AssetResponseDto>;
let _selectedGroup: Set<string>;
let _assetsInAlbums: AssetResponseDto[];
let _assetSelectionCandidates: Set<AssetResponseDto>;
let _assetSelectionStart: AssetResponseDto | null;
// Subscriber
assetGridState.subscribe((state) => {
_assetGridState = state;
});
const assetsInAlbumStoreState = writable<AssetResponseDto[]>([]);
// Selected assets
const selectedAssets = writable<Set<AssetResponseDto>>(new Set());
// Selected date groups
const selectedGroup = writable<Set<string>>(new Set());
// If any asset selected
const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
viewingAssetStoreState.subscribe((asset) => {
_viewingAssetStoreState = asset;
});
// 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<Set<AssetResponseDto>>(new Set());
// The beginning of the selection range
const assetSelectionStart = writable<AssetResponseDto | null>(null);
selectedAssets.subscribe((assets) => {
_selectedAssets = assets;
@ -64,89 +83,7 @@ function createAssetInteractionStore() {
assetSelectionStart.subscribe((asset) => {
_assetSelectionStart = asset;
});
// Methods
/**
* Asset Viewer
*/
const setViewingAsset = async (asset: AssetResponseDto) => {
setViewingAssetId(asset.id);
};
const setViewingAssetId = async (id: string) => {
const { data } = await api.assetApi.getAssetById({ id });
viewingAssetStoreState.set(data);
isViewingAssetStoreState.set(true);
};
const setIsViewingAsset = (isViewing: boolean) => {
isViewingAssetStoreState.set(isViewing);
};
const getNextAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => {
const currentBucket = _assetGridState.buckets[currentBucketIndex];
const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId);
if (assetIndex === -1) {
return null;
}
if (assetIndex + 1 < currentBucket.assets.length) {
return currentBucket.assets[assetIndex + 1];
}
const nextBucketIndex = currentBucketIndex + 1;
if (nextBucketIndex >= _assetGridState.buckets.length) {
return null;
}
const nextBucket = _assetGridState.buckets[nextBucketIndex];
await assetStore.getAssetsByBucket(nextBucket.bucketDate, BucketPosition.Unknown);
return nextBucket.assets[0] ?? null;
};
const getPrevAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => {
const currentBucket = _assetGridState.buckets[currentBucketIndex];
const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId);
if (assetIndex === -1) {
return null;
}
if (assetIndex > 0) {
return currentBucket.assets[assetIndex - 1];
}
const prevBucketIndex = currentBucketIndex - 1;
if (prevBucketIndex < 0) {
return null;
}
const prevBucket = _assetGridState.buckets[prevBucketIndex];
await assetStore.getAssetsByBucket(prevBucket.bucketDate, BucketPosition.Unknown);
return prevBucket.assets[prevBucket.assets.length - 1] ?? null;
};
const navigateAsset = async (direction: 'next' | 'previous') => {
const currentAssetId = _viewingAssetStoreState.id;
const currentBucketIndex = _assetGridState.loadedAssets[currentAssetId];
if (currentBucketIndex < 0 || currentBucketIndex >= _assetGridState.buckets.length) {
return;
}
const asset =
direction === 'next'
? await getNextAsset(currentBucketIndex, currentAssetId)
: await getPrevAsset(currentBucketIndex, currentAssetId);
if (asset) {
setViewingAsset(asset);
}
};
/**
* Multiselect
*/
const addAssetToMultiselectGroup = (asset: AssetResponseDto) => {
// Not select if in album already
if (_assetsInAlbums.find((a) => a.id === asset.id)) {
@ -205,10 +142,6 @@ function createAssetInteractionStore() {
};
return {
setViewingAsset,
setViewingAssetId,
setIsViewingAsset,
navigateAsset,
addAssetToMultiselectGroup,
removeAssetFromMultiselectGroup,
addGroupToMultiselectGroup,
@ -217,7 +150,24 @@ function createAssetInteractionStore() {
clearAssetSelectionCandidates,
setAssetSelectionStart,
clearMultiselect,
isMultiSelectState: {
subscribe: isMultiSelectStoreState.subscribe,
},
assetsInAlbumState: {
subscribe: assetsInAlbumStoreState.subscribe,
set: assetsInAlbumStoreState.set,
},
selectedAssets: {
subscribe: selectedAssets.subscribe,
},
selectedGroup: {
subscribe: selectedGroup.subscribe,
},
assetSelectionCandidates: {
subscribe: assetSelectionCandidates.subscribe,
},
assetSelectionStart: {
subscribe: assetSelectionStart.subscribe,
},
};
}
export const assetInteractionStore = createAssetInteractionStore();

View file

@ -0,0 +1,31 @@
import { writable } from 'svelte/store';
import { api, type AssetResponseDto } from '@api';
function createAssetViewingStore() {
const viewingAssetStoreState = writable<AssetResponseDto>();
const viewState = writable<boolean>(false);
const setAssetId = async (id: string) => {
const { data } = await api.assetApi.getAssetById({ id });
viewingAssetStoreState.set(data);
viewState.set(true);
};
const showAssetViewer = (show: boolean) => {
viewState.set(show);
};
return {
asset: {
subscribe: viewingAssetStoreState.subscribe,
},
isViewing: {
subscribe: viewState.subscribe,
set: viewState.set,
},
setAssetId,
showAssetViewer,
};
}
export const assetViewingStore = createAssetViewingStore();

View file

@ -1,25 +1,34 @@
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
import { api, AssetCountByTimeBucketResponseDto } from '@api';
import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto } from '@api';
import { writable } from 'svelte/store';
/**
* The state that holds information about the asset grid
*/
export const assetGridState = writable<AssetGridState>(new AssetGridState());
export const loadingBucketState = writable<{ [key: string]: boolean }>({});
export interface AssetStore {
setInitialState: (
viewportHeight: number,
viewportWidth: number,
data: AssetCountByTimeBucketResponseDto,
userId: string | undefined,
) => void;
getAssetsByBucket: (bucket: string, position: BucketPosition) => Promise<void>;
updateBucketHeight: (bucket: string, actualBucketHeight: number) => number;
cancelBucketRequest: (token: AbortController, bucketDate: string) => Promise<void>;
getAdjacentAsset: (assetId: string, direction: 'next' | 'previous') => Promise<string | null>;
removeAsset: (assetId: string) => void;
updateAsset: (assetId: string, isFavorite: boolean) => void;
subscribe: (run: (value: AssetGridState) => void, invalidate?: (value?: AssetGridState) => void) => () => void;
}
function createAssetStore() {
export function createAssetStore(): AssetStore {
let _loadingBuckets: { [key: string]: boolean } = {};
let _assetGridState = new AssetGridState();
assetGridState.subscribe((state) => {
const { subscribe, set, update } = writable(new AssetGridState());
subscribe((state) => {
_assetGridState = state;
});
let _loadingBucketState: { [key: string]: boolean } = {};
loadingBucketState.subscribe((state) => {
_loadingBucketState = state;
});
const estimateViewportHeight = (assetCount: number, viewportWidth: number): number => {
const _estimateViewportHeight = (assetCount: number, viewportWidth: number): number => {
// Ideally we would use the average aspect ratio for the photoset, however assume
// a normal landscape aspect ratio of 3:2, then discount for the likelihood we
// will be scaling down and coalescing.
@ -39,25 +48,19 @@ function createAssetStore() {
);
};
/**
* Set initial state
* @param viewportHeight
* @param viewportWidth
* @param data
*/
const setInitialState = (
viewportHeight: number,
viewportWidth: number,
data: AssetCountByTimeBucketResponseDto,
userId: string | undefined,
) => {
assetGridState.set({
set({
viewportHeight,
viewportWidth,
timelineHeight: 0,
buckets: data.buckets.map((bucket) => ({
bucketDate: bucket.timeBucket,
bucketHeight: estimateViewportHeight(bucket.count, viewportWidth),
bucketHeight: _estimateViewportHeight(bucket.count, viewportWidth),
assets: [],
cancelToken: new AbortController(),
position: BucketPosition.Unknown,
@ -67,8 +70,7 @@ function createAssetStore() {
userId,
});
// Update timeline height based on calculated bucket height
assetGridState.update((state) => {
update((state) => {
state.timelineHeight = state.buckets.reduce((acc, b) => acc + b.bucketHeight, 0);
return state;
});
@ -78,7 +80,7 @@ function createAssetStore() {
try {
const currentBucketData = _assetGridState.buckets.find((b) => b.bucketDate === bucket);
if (currentBucketData?.assets && currentBucketData.assets.length > 0) {
assetGridState.update((state) => {
update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
state.buckets[bucketIndex].position = position;
return state;
@ -86,10 +88,7 @@ function createAssetStore() {
return;
}
loadingBucketState.set({
..._loadingBucketState,
[bucket]: true,
});
_loadingBuckets = { ..._loadingBuckets, [bucket]: true };
const { data: assets } = await api.assetApi.getAssetByTimeBucket(
{
getAssetByTimeBucketDto: {
@ -100,13 +99,9 @@ function createAssetStore() {
},
{ signal: currentBucketData?.cancelToken.signal },
);
loadingBucketState.set({
..._loadingBucketState,
[bucket]: false,
});
_loadingBuckets = { ..._loadingBuckets, [bucket]: false };
// Update assetGridState with assets by time bucket
assetGridState.update((state) => {
update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
state.buckets[bucketIndex].assets = assets;
state.buckets[bucketIndex].position = position;
@ -125,7 +120,7 @@ function createAssetStore() {
};
const removeAsset = (assetId: string) => {
assetGridState.update((state) => {
update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.assets.some((a) => a.id === assetId));
const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId);
state.buckets[bucketIndex].assets.splice(assetIndex, 1);
@ -140,7 +135,7 @@ function createAssetStore() {
};
const _removeBucket = (bucketDate: string) => {
assetGridState.update((state) => {
update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate);
state.buckets.splice(bucketIndex, 1);
state.assets = state.buckets.flatMap((b) => b.assets);
@ -153,7 +148,7 @@ function createAssetStore() {
let scrollTimeline = false;
let heightDelta = 0;
assetGridState.update((state) => {
update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
// Update timeline height based on the new bucket height
const estimateBucketHeight = state.buckets[bucketIndex].bucketHeight;
@ -177,9 +172,13 @@ function createAssetStore() {
};
const cancelBucketRequest = async (token: AbortController, bucketDate: string) => {
if (!_loadingBuckets[bucketDate]) {
return;
}
token.abort();
// set new abort controller for bucket
assetGridState.update((state) => {
update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate);
state.buckets[bucketIndex].cancelToken = new AbortController();
return state;
@ -187,7 +186,7 @@ function createAssetStore() {
};
const updateAsset = (assetId: string, isFavorite: boolean) => {
assetGridState.update((state) => {
update((state) => {
const bucketIndex = state.buckets.findIndex((b) => b.assets.some((a) => a.id === assetId));
const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId);
state.buckets[bucketIndex].assets[assetIndex].isFavorite = isFavorite;
@ -198,14 +197,72 @@ function createAssetStore() {
});
};
const _getNextAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => {
const currentBucket = _assetGridState.buckets[currentBucketIndex];
const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId);
if (assetIndex === -1) {
return null;
}
if (assetIndex + 1 < currentBucket.assets.length) {
return currentBucket.assets[assetIndex + 1];
}
const nextBucketIndex = currentBucketIndex + 1;
if (nextBucketIndex >= _assetGridState.buckets.length) {
return null;
}
const nextBucket = _assetGridState.buckets[nextBucketIndex];
await getAssetsByBucket(nextBucket.bucketDate, BucketPosition.Unknown);
return nextBucket.assets[0] ?? null;
};
const _getPrevAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => {
const currentBucket = _assetGridState.buckets[currentBucketIndex];
const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId);
if (assetIndex === -1) {
return null;
}
if (assetIndex > 0) {
return currentBucket.assets[assetIndex - 1];
}
const prevBucketIndex = currentBucketIndex - 1;
if (prevBucketIndex < 0) {
return null;
}
const prevBucket = _assetGridState.buckets[prevBucketIndex];
await getAssetsByBucket(prevBucket.bucketDate, BucketPosition.Unknown);
return prevBucket.assets[prevBucket.assets.length - 1] ?? null;
};
const getAdjacentAsset = async (assetId: string, direction: 'next' | 'previous'): Promise<string | null> => {
const currentBucketIndex = _assetGridState.loadedAssets[assetId];
if (currentBucketIndex < 0 || currentBucketIndex >= _assetGridState.buckets.length) {
return null;
}
const asset =
direction === 'next'
? await _getNextAsset(currentBucketIndex, assetId)
: await _getPrevAsset(currentBucketIndex, assetId);
return asset?.id ?? null;
};
return {
setInitialState,
getAssetsByBucket,
removeAsset,
updateBucketHeight,
cancelBucketRequest,
getAdjacentAsset,
updateAsset,
subscribe,
};
}
export const assetStore = createAssetStore();

View file

@ -3,11 +3,6 @@
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import {
assetInteractionStore,
isViewingAssetStoreState,
viewingAssetStoreState,
} from '$lib/stores/asset-interaction.store';
import { mapSettings } from '$lib/stores/preferences.store';
import { MapMarkerResponseDto, api } from '@api';
import { isEqual, omit } from 'lodash-es';
@ -15,9 +10,12 @@
import Cog from 'svelte-material-icons/Cog.svelte';
import type { PageData } from './$types';
import { DateTime, Duration } from 'luxon';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
export let data: PageData;
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore;
let leaflet: typeof import('$lib/components/shared-components/leaflet');
let mapMarkers: MapMarkerResponseDto[] = [];
let abortController: AbortController;
@ -34,8 +32,7 @@
if (abortController) {
abortController.abort();
}
assetInteractionStore.clearMultiselect();
assetInteractionStore.setIsViewingAsset(false);
assetViewingStore.showAssetViewer(false);
});
async function loadMapMarkers() {
@ -83,20 +80,20 @@
}
function onViewAssets(assetIds: string[], activeAssetIndex: number) {
assetInteractionStore.setViewingAssetId(assetIds[activeAssetIndex]);
assetViewingStore.setAssetId(assetIds[activeAssetIndex]);
viewingAssets = assetIds;
viewingAssetCursor = activeAssetIndex;
}
function navigateNext() {
if (viewingAssetCursor < viewingAssets.length - 1) {
assetInteractionStore.setViewingAssetId(viewingAssets[++viewingAssetCursor]);
assetViewingStore.setAssetId(viewingAssets[++viewingAssetCursor]);
}
}
function navigatePrevious() {
if (viewingAssetCursor > 0) {
assetInteractionStore.setViewingAssetId(viewingAssets[--viewingAssetCursor]);
assetViewingStore.setAssetId(viewingAssets[--viewingAssetCursor]);
}
}
</script>
@ -142,14 +139,14 @@
</UserPageLayout>
<Portal target="body">
{#if $isViewingAssetStoreState}
{#if $showAssetViewer}
<AssetViewer
asset={$viewingAssetStoreState}
asset={$viewingAsset}
showNavigation={viewingAssets.length > 1}
on:navigate-next={navigateNext}
on:navigate-previous={navigatePrevious}
on:close={() => {
assetInteractionStore.setIsViewingAsset(false);
assetViewingStore.showAssetViewer(false);
}}
/>
{/if}

View file

@ -8,21 +8,26 @@
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { AppRoute } from '$lib/constants';
import { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
import { onDestroy } from 'svelte';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import Plus from 'svelte-material-icons/Plus.svelte';
import type { PageData } from './$types';
import { createAssetStore } from '$lib/stores/assets.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
export let data: PageData;
const assetStore = createAssetStore();
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
onDestroy(() => {
assetInteractionStore.clearMultiselect();
});
</script>
<main class="grid h-screen bg-immich-bg pt-18 dark:bg-immich-dark-bg">
{#if $isMultiSelectStoreState}
{#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
<DownloadAction />
</AssetSelectControlBar>
@ -44,5 +49,5 @@
</svelte:fragment>
</ControlAppBar>
{/if}
<AssetGrid user={data.partner} />
<AssetGrid {assetStore} {assetInteractionStore} user={data.partner} />
</main>

View file

@ -11,8 +11,8 @@
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
import { assetStore } from '$lib/stores/assets.store';
import { createAssetStore } from '$lib/stores/assets.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { api } from '@api';
import { onDestroy, onMount } from 'svelte';
@ -23,6 +23,10 @@
export let data: PageData;
let assetCount = 1;
const assetStore = createAssetStore();
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
onMount(async () => {
const { data: stats } = await api.assetApi.getAssetStats();
assetCount = stats.total;
@ -39,12 +43,12 @@
};
</script>
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectStoreState} showUploadButton>
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
<svelte:fragment slot="header">
{#if $isMultiSelectStoreState}
{#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
<CreateSharedLink />
<SelectAllAssets />
<SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={Plus} title="Add">
<AddToAlbum />
<AddToAlbum shared />
@ -60,7 +64,7 @@
</svelte:fragment>
<svelte:fragment slot="content">
{#if assetCount}
<AssetGrid showMemoryLane />
<AssetGrid {assetStore} {assetInteractionStore} showMemoryLane />
{:else}
<EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={handleUpload} />
{/if}

View file

@ -25,10 +25,12 @@
import { flip } from 'svelte/animate';
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import { isViewingAssetStoreState } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
export let data: PageData;
let { isViewing: showAssetViewer } = assetViewingStore;
// The GalleryViewer pushes it's own history state, which causes weird
// behavior for history.back(). To prevent that we store the previous page
// manually and navigate back to that.
@ -48,7 +50,7 @@
});
const handleKeyboardPress = (event: KeyboardEvent) => {
if (!$isViewingAssetStoreState) {
if (!$showAssetViewer) {
switch (event.key) {
case 'Escape':
goto(previousRoute);