From 28ab1d455145081aca6fce42ca29af1ef1bd79d6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 2 Aug 2023 21:57:11 -0400 Subject: [PATCH] refactor(web): asset grid state (#3513) * refactor(web): asset grid state * fix: multi-select across time buckets --------- Co-authored-by: Alex Tran --- .../actions/select-all-assets.svelte | 2 +- .../photos-page/asset-date-group.svelte | 2 +- .../components/photos-page/asset-grid.svelte | 58 ++-- web/src/lib/models/asset-grid-state.ts | 256 ++++++++++++++-- web/src/lib/stores/assets.store.ts | 282 ++---------------- 5 files changed, 271 insertions(+), 329 deletions(-) diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index 6b0241d51..b35e7f192 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -19,7 +19,7 @@ const assetGridState = get(assetStore); for (const bucket of assetGridState.buckets) { - await assetStore.getAssetsByBucket(bucket.bucketDate, BucketPosition.Unknown); + await assetStore.loadBucket(bucket.bucketDate, BucketPosition.Unknown); for (const asset of bucket.assets) { assetInteractionStore.addAssetToMultiselectGroup(asset); } diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index f8cb49648..b287f265a 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -60,7 +60,7 @@ $: { if (actualBucketHeight && actualBucketHeight != 0 && actualBucketHeight != bucketHeight) { - const heightDelta = assetStore.updateBucketHeight(bucketDate, actualBucketHeight); + const heightDelta = assetStore.updateBucket(bucketDate, actualBucketHeight); if (heightDelta !== 0) { scrollTimeline(heightDelta); } diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 761e8e43c..acd8314eb 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -4,7 +4,7 @@ import { locale } from '$lib/stores/preferences.store'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import type { UserResponseDto } from '@api'; - import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum } from '@api'; + import { AssetResponseDto, TimeGroupEnum, api } from '@api'; import { DateTime } from 'luxon'; import { onDestroy, onMount } from 'svelte'; import AssetViewer from '../asset-viewer/asset-viewer.svelte'; @@ -17,13 +17,13 @@ import AssetDateGroup from './asset-date-group.svelte'; import MemoryLane from './memory-lane.svelte'; - import { AppRoute } from '$lib/constants'; - import { goto } from '$app/navigation'; import { browser } from '$app/environment'; + import { goto } from '$app/navigation'; + import { AppRoute } from '$lib/constants'; + import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; + import type { AssetStore } from '$lib/stores/assets.store'; 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; @@ -39,14 +39,13 @@ let viewportHeight = 0; let viewportWidth = 0; let assetGridElement: HTMLElement; - let bucketInfo: AssetCountByTimeBucketResponseDto; let showShortcuts = false; const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); onMount(async () => { document.addEventListener('keydown', onKeyboardPress); - const { data: assetCountByTimebucket } = await api.assetApi.getAssetCountByTimeBucket({ + const { data: timeBuckets } = await api.assetApi.getAssetCountByTimeBucket({ getAssetCountByTimeBucketDto: { timeGroup: TimeGroupEnum.Month, userId: user?.id, @@ -54,26 +53,7 @@ }, }); - bucketInfo = assetCountByTimebucket; - - assetStore.setInitialState(viewportHeight, viewportWidth, assetCountByTimebucket, user?.id); - - // Get asset bucket if bucket height is smaller than viewport height - let bucketsToFetchInitially: string[] = []; - let initialBucketsHeight = 0; - $assetStore.buckets.every((bucket) => { - if (initialBucketsHeight < viewportHeight) { - initialBucketsHeight += bucket.bucketHeight; - bucketsToFetchInitially.push(bucket.bucketDate); - return true; - } else { - return false; - } - }); - - bucketsToFetchInitially.forEach((bucketDate) => { - assetStore.getAssetsByBucket(bucketDate, BucketPosition.Visible); - }); + assetStore.init({ width: viewportHeight, height: viewportWidth }, timeBuckets.buckets, user?.id); }); onDestroy(() => { @@ -81,7 +61,7 @@ document.removeEventListener('keydown', onKeyboardPress); } - assetStore.setInitialState(0, 0, { totalCount: 0, buckets: [] }, undefined); + assetStore.init({ width: 0, height: 0 }, [], undefined); }); const handleKeyboardPress = (event: KeyboardEvent) => { @@ -113,7 +93,7 @@ const target = el.firstChild as HTMLElement; if (target) { const bucketDate = target.id.split('_')[1]; - assetStore.getAssetsByBucket(bucketDate, event.detail.position); + assetStore.loadBucket(bucketDate, event.detail.position); } } @@ -122,14 +102,14 @@ } const navigateToPreviousAsset = async () => { - const prevAsset = await assetStore.getAdjacentAsset($viewingAsset.id, 'previous'); + const prevAsset = await assetStore.getPreviousAssetId($viewingAsset.id); if (prevAsset) { assetViewingStore.setAssetId(prevAsset); } }; const navigateToNextAsset = async () => { - const nextAsset = await assetStore.getAdjacentAsset($viewingAsset.id, 'next'); + const nextAsset = await assetStore.getNextAssetId($viewingAsset.id); if (nextAsset) { assetViewingStore.setAssetId(nextAsset); } @@ -234,8 +214,12 @@ assetInteractionStore.clearAssetSelectionCandidates(); if ($assetSelectionStart && rangeSelection) { - let startBucketIndex = $assetStore.loadedAssets[$assetSelectionStart.id]; - let endBucketIndex = $assetStore.loadedAssets[asset.id]; + let startBucketIndex = $assetStore.getBucketIndexByAssetId($assetSelectionStart.id); + let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id); + + if (startBucketIndex === null || endBucketIndex === null) { + return; + } if (endBucketIndex < startBucketIndex) { [startBucketIndex, endBucketIndex] = [endBucketIndex, startBucketIndex]; @@ -244,7 +228,7 @@ // Select/deselect assets in all intermediate buckets for (let bucketIndex = startBucketIndex + 1; bucketIndex < endBucketIndex; bucketIndex++) { const bucket = $assetStore.buckets[bucketIndex]; - await assetStore.getAssetsByBucket(bucket.bucketDate, BucketPosition.Unknown); + await assetStore.loadBucket(bucket.bucketDate, BucketPosition.Unknown); for (const asset of bucket.assets) { if (deselect) { assetInteractionStore.removeAssetFromMultiselectGroup(asset); @@ -308,7 +292,7 @@ (showShortcuts = !showShortcuts)} /> {/if} -{#if bucketInfo && viewportHeight && $assetStore.timelineHeight > viewportHeight} +{#if viewportHeight && $assetStore.initialized && $assetStore.timelineHeight > viewportHeight} { - await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate); - }} + on:hidden={() => assetStore.cancelBucket(bucket)} let:intersecting top={750} bottom={750} diff --git a/web/src/lib/models/asset-grid-state.ts b/web/src/lib/models/asset-grid-state.ts index 35a3b2a5a..6d692f901 100644 --- a/web/src/lib/models/asset-grid-state.ts +++ b/web/src/lib/models/asset-grid-state.ts @@ -1,4 +1,7 @@ -import type { AssetResponseDto } from '@api'; +import { api, AssetCountByTimeBucket, AssetResponseDto } from '@api'; +import { writable } from 'svelte/store'; +import type { AssetStore } from '../stores/assets.store'; +import { handleError } from '../utils/handle-error'; export enum BucketPosition { Above = 'above', @@ -7,6 +10,17 @@ export enum BucketPosition { Unknown = 'unknown', } +export interface Viewport { + width: number; + height: number; +} + +interface AssetLookup { + bucket: AssetBucket; + bucketIndex: number; + assetIndex: number; +} + export class AssetBucket { /** * The DOM height of the bucket in pixel @@ -15,44 +29,220 @@ export class AssetBucket { bucketHeight!: number; bucketDate!: string; assets!: AssetResponseDto[]; - cancelToken!: AbortController; + cancelToken!: AbortController | null; position!: BucketPosition; } -export class AssetGridState { - /** - * The total height of the timeline in pixel - * This value is first estimated by the number of asset and later is corrected as the user scroll - */ +const THUMBNAIL_HEIGHT = 235; + +export class AssetGridState implements AssetStore { + private store$ = writable(this); + private assetToBucket: Record = {}; + private viewport: Viewport = { width: 0, height: 0 }; + private userId: string | undefined; + + initialized = false; timelineHeight = 0; - - /** - * The fixed viewport height in pixel - */ - viewportHeight = 0; - - /** - * The fixed viewport width in pixel - */ - viewportWidth = 0; - - /** - * List of bucket information - */ buckets: AssetBucket[] = []; - - /** - * Total assets that have been loaded - */ assets: AssetResponseDto[] = []; - /** - * Total assets that have been loaded along with additional data - */ - loadedAssets: Record = {}; + subscribe = this.store$.subscribe; - /** - * User that owns assets - */ - userId: string | undefined; + init(viewport: Viewport, buckets: AssetCountByTimeBucket[], userId: string | undefined) { + this.initialized = false; + this.assets = []; + this.assetToBucket = {}; + this.buckets = []; + this.viewport = viewport; + this.userId = userId; + this.buckets = buckets.map((bucket) => { + const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10); + const rows = Math.ceil(unwrappedWidth / this.viewport.width); + const height = rows * THUMBNAIL_HEIGHT; + + return { + bucketDate: bucket.timeBucket, + bucketHeight: height, + assets: [], + cancelToken: null, + position: BucketPosition.Unknown, + }; + }); + + this.timelineHeight = this.buckets.reduce((acc, b) => acc + b.bucketHeight, 0); + + this.emit(false); + + let height = 0; + for (const bucket of this.buckets) { + if (height < this.viewport.height) { + height += bucket.bucketHeight; + this.loadBucket(bucket.bucketDate, BucketPosition.Visible); + continue; + } + + break; + } + + this.initialized = true; + } + + getBucketByDate(bucketDate: string): AssetBucket | null { + return this.buckets.find((bucket) => bucket.bucketDate === bucketDate) || null; + } + + getBucketInfoForAssetId(assetId: string) { + return this.assetToBucket[assetId] || null; + } + + getBucketIndexByAssetId(assetId: string) { + return this.assetToBucket[assetId]?.bucketIndex ?? null; + } + + async loadBucket(bucketDate: string, position: BucketPosition): Promise { + try { + const bucket = this.getBucketByDate(bucketDate); + if (!bucket) { + return; + } + + bucket.position = position; + + if (bucket.assets.length !== 0) { + this.emit(false); + return; + } + + bucket.cancelToken = new AbortController(); + + const { data: assets } = await api.assetApi.getAssetByTimeBucket( + { + getAssetByTimeBucketDto: { + timeBucket: [bucketDate], + userId: this.userId, + withoutThumbs: true, + }, + }, + { signal: bucket.cancelToken.signal }, + ); + + bucket.assets = assets; + this.emit(true); + } catch (error) { + handleError(error, 'Failed to load assets'); + } + } + + cancelBucket(bucket: AssetBucket) { + bucket.cancelToken?.abort(); + } + + updateBucket(bucketDate: string, height: number) { + const bucket = this.getBucketByDate(bucketDate); + if (!bucket) { + return 0; + } + + const delta = height - bucket.bucketHeight; + const scrollTimeline = bucket.position == BucketPosition.Above; + + bucket.bucketHeight = height; + bucket.position = BucketPosition.Unknown; + + this.timelineHeight += delta; + + this.emit(false); + + return scrollTimeline ? delta : 0; + } + + updateAsset(assetId: string, isFavorite: boolean) { + const asset = this.assets.find((asset) => asset.id === assetId); + if (!asset) { + return; + } + + asset.isFavorite = isFavorite; + this.emit(false); + } + + removeAsset(assetId: string) { + for (let i = 0; i < this.buckets.length; i++) { + const bucket = this.buckets[i]; + for (let j = 0; j < bucket.assets.length; j++) { + const asset = bucket.assets[j]; + if (asset.id !== assetId) { + continue; + } + + bucket.assets.splice(j, 1); + if (bucket.assets.length === 0) { + this.buckets.splice(i, 1); + } + + this.emit(true); + return; + } + } + } + + async getPreviousAssetId(assetId: string): Promise { + const info = this.getBucketInfoForAssetId(assetId); + if (!info) { + return null; + } + + const { bucket, assetIndex, bucketIndex } = info; + + if (assetIndex !== 0) { + return bucket.assets[assetIndex - 1].id; + } + + if (bucketIndex === 0) { + return null; + } + + const previousBucket = this.buckets[bucketIndex - 1]; + await this.loadBucket(previousBucket.bucketDate, BucketPosition.Unknown); + return previousBucket.assets.at(-1)?.id || null; + } + + async getNextAssetId(assetId: string): Promise { + const info = this.getBucketInfoForAssetId(assetId); + if (!info) { + return null; + } + + const { bucket, assetIndex, bucketIndex } = info; + + if (assetIndex !== bucket.assets.length - 1) { + return bucket.assets[assetIndex + 1].id; + } + + if (bucketIndex === this.buckets.length - 1) { + return null; + } + + const nextBucket = this.buckets[bucketIndex + 1]; + await this.loadBucket(nextBucket.bucketDate, BucketPosition.Unknown); + return nextBucket.assets[0]?.id || null; + } + + private emit(recalculate: boolean) { + if (recalculate) { + this.assets = this.buckets.flatMap(({ assets }) => assets); + + const assetToBucket: Record = {}; + for (let i = 0; i < this.buckets.length; i++) { + const bucket = this.buckets[i]; + for (let j = 0; j < bucket.assets.length; j++) { + const asset = bucket.assets[j]; + assetToBucket[asset.id] = { bucket, bucketIndex: i, assetIndex: j }; + } + } + this.assetToBucket = assetToBucket; + } + + this.store$.update(() => this); + } } diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 78a1cf786..1cd9a3e00 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -1,268 +1,38 @@ -import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state'; -import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto } from '@api'; -import { writable } from 'svelte/store'; +import { AssetBucket, AssetGridState, BucketPosition, Viewport } from '$lib/models/asset-grid-state'; +import type { AssetCountByTimeBucket } from '@api'; export interface AssetStore { - setInitialState: ( - viewportHeight: number, - viewportWidth: number, - data: AssetCountByTimeBucketResponseDto, - userId: string | undefined, - ) => void; - getAssetsByBucket: (bucket: string, position: BucketPosition) => Promise; - updateBucketHeight: (bucket: string, actualBucketHeight: number) => number; - cancelBucketRequest: (token: AbortController, bucketDate: string) => Promise; - getAdjacentAsset: (assetId: string, direction: 'next' | 'previous') => Promise; + init: (viewport: Viewport, data: AssetCountByTimeBucket[], userId: string | undefined) => void; + + // bucket + loadBucket: (bucket: string, position: BucketPosition) => Promise; + updateBucket: (bucket: string, actualBucketHeight: number) => number; + cancelBucket: (bucket: AssetBucket) => void; + + // asset removeAsset: (assetId: string) => void; updateAsset: (assetId: string, isFavorite: boolean) => void; + + // asset navigation + getNextAssetId: (assetId: string) => Promise; + getPreviousAssetId: (assetId: string) => Promise; + + // store subscribe: (run: (value: AssetGridState) => void, invalidate?: (value?: AssetGridState) => void) => () => void; } export function createAssetStore(): AssetStore { - let _loadingBuckets: { [key: string]: boolean } = {}; - let _assetGridState = new AssetGridState(); - - const { subscribe, set, update } = writable(new AssetGridState()); - - subscribe((state) => { - _assetGridState = state; - }); - - 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. - const thumbnailHeight = 235; - const unwrappedWidth = (3 / 2) * assetCount * thumbnailHeight * (7 / 10); - const rows = Math.ceil(unwrappedWidth / viewportWidth); - const height = rows * thumbnailHeight; - return height; - }; - - const refreshLoadedAssets = (state: AssetGridState): void => { - state.loadedAssets = {}; - state.buckets.forEach((bucket, bucketIndex) => - bucket.assets.map((asset) => { - state.loadedAssets[asset.id] = bucketIndex; - }), - ); - }; - - const setInitialState = ( - viewportHeight: number, - viewportWidth: number, - data: AssetCountByTimeBucketResponseDto, - userId: string | undefined, - ) => { - set({ - viewportHeight, - viewportWidth, - timelineHeight: 0, - buckets: data.buckets.map((bucket) => ({ - bucketDate: bucket.timeBucket, - bucketHeight: _estimateViewportHeight(bucket.count, viewportWidth), - assets: [], - cancelToken: new AbortController(), - position: BucketPosition.Unknown, - })), - assets: [], - loadedAssets: {}, - userId, - }); - - update((state) => { - state.timelineHeight = state.buckets.reduce((acc, b) => acc + b.bucketHeight, 0); - return state; - }); - }; - - const getAssetsByBucket = async (bucket: string, position: BucketPosition) => { - try { - const currentBucketData = _assetGridState.buckets.find((b) => b.bucketDate === bucket); - if (currentBucketData?.assets && currentBucketData.assets.length > 0) { - update((state) => { - const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket); - state.buckets[bucketIndex].position = position; - return state; - }); - return; - } - - _loadingBuckets = { ..._loadingBuckets, [bucket]: true }; - const { data: assets } = await api.assetApi.getAssetByTimeBucket( - { - getAssetByTimeBucketDto: { - timeBucket: [bucket], - userId: _assetGridState.userId, - withoutThumbs: true, - }, - }, - { signal: currentBucketData?.cancelToken.signal }, - ); - _loadingBuckets = { ..._loadingBuckets, [bucket]: false }; - - update((state) => { - const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket); - state.buckets[bucketIndex].assets = assets; - state.buckets[bucketIndex].position = position; - state.assets = state.buckets.flatMap((b) => b.assets); - refreshLoadedAssets(state); - return state; - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - if (e.name === 'CanceledError') { - return; - } - console.error('Failed to get asset for bucket ', bucket); - console.error(e); - } - }; - - const removeAsset = (assetId: string) => { - 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); - - if (state.buckets[bucketIndex].assets.length === 0) { - _removeBucket(state.buckets[bucketIndex].bucketDate); - } - state.assets = state.buckets.flatMap((b) => b.assets); - refreshLoadedAssets(state); - return state; - }); - }; - - const _removeBucket = (bucketDate: string) => { - update((state) => { - const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate); - state.buckets.splice(bucketIndex, 1); - state.assets = state.buckets.flatMap((b) => b.assets); - refreshLoadedAssets(state); - return state; - }); - }; - - const updateBucketHeight = (bucket: string, actualBucketHeight: number): number => { - let scrollTimeline = false; - let heightDelta = 0; - - 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; - - heightDelta = actualBucketHeight - estimateBucketHeight; - state.timelineHeight += heightDelta; - - scrollTimeline = state.buckets[bucketIndex].position == BucketPosition.Above; - - state.buckets[bucketIndex].bucketHeight = actualBucketHeight; - state.buckets[bucketIndex].position = BucketPosition.Unknown; - - return state; - }); - - if (scrollTimeline) { - return heightDelta; - } - - return 0; - }; - - const cancelBucketRequest = async (token: AbortController, bucketDate: string) => { - if (!_loadingBuckets[bucketDate]) { - return; - } - - token.abort(); - - update((state) => { - const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate); - state.buckets[bucketIndex].cancelToken = new AbortController(); - return state; - }); - }; - - const updateAsset = (assetId: string, isFavorite: boolean) => { - 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; - - state.assets = state.buckets.flatMap((b) => b.assets); - refreshLoadedAssets(state); - return state; - }); - }; - - const _getNextAsset = async (currentBucketIndex: number, assetId: string): Promise => { - 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 => { - 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 => { - 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; - }; + const store = new AssetGridState(); return { - setInitialState, - getAssetsByBucket, - removeAsset, - updateBucketHeight, - cancelBucketRequest, - getAdjacentAsset, - updateAsset, - subscribe, + init: store.init.bind(store), + loadBucket: store.loadBucket.bind(store), + updateBucket: store.updateBucket.bind(store), + cancelBucket: store.cancelBucket.bind(store), + removeAsset: store.removeAsset.bind(store), + updateAsset: store.updateAsset.bind(store), + getNextAssetId: store.getNextAssetId.bind(store), + getPreviousAssetId: store.getPreviousAssetId.bind(store), + subscribe: store.subscribe, }; }