mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
Feature - Implemented virtual scroll on web (#573)
This PR implemented a virtual scroll on the web, as seen in this article. [Building the Google Photos Web UI](https://medium.com/google-design/google-photos-45b714dfbed1)
This commit is contained in:
parent
bd92dde117
commit
552340add7
58 changed files with 2197 additions and 698 deletions
139
web/src/lib/stores/assets.store.ts
Normal file
139
web/src/lib/stores/assets.store.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { writable, derived, readable } from 'svelte/store';
|
||||
import lodash from 'lodash-es';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto } from '@api';
|
||||
import { AssetGridState } from '$lib/models/asset-grid-state';
|
||||
import { calculateViewportHeightByNumberOfAsset } from '$lib/utils/viewport-utils';
|
||||
|
||||
/**
|
||||
* The state that holds information about the asset grid
|
||||
*/
|
||||
export const assetGridState = writable<AssetGridState>(new AssetGridState());
|
||||
export const loadingBucketState = writable<{ [key: string]: boolean }>({});
|
||||
|
||||
function createAssetStore() {
|
||||
let _assetGridState = new AssetGridState();
|
||||
assetGridState.subscribe((state) => {
|
||||
_assetGridState = state;
|
||||
});
|
||||
|
||||
let _loadingBucketState: { [key: string]: boolean } = {};
|
||||
loadingBucketState.subscribe((state) => {
|
||||
_loadingBucketState = state;
|
||||
});
|
||||
/**
|
||||
* Set intial state
|
||||
* @param viewportHeight
|
||||
* @param viewportWidth
|
||||
* @param data
|
||||
*/
|
||||
const setInitialState = (
|
||||
viewportHeight: number,
|
||||
viewportWidth: number,
|
||||
data: AssetCountByTimeBucketResponseDto
|
||||
) => {
|
||||
assetGridState.set({
|
||||
viewportHeight,
|
||||
viewportWidth,
|
||||
timelineHeight: calculateViewportHeightByNumberOfAsset(data.totalCount, viewportWidth),
|
||||
buckets: data.buckets.map((d) => ({
|
||||
bucketDate: d.timeBucket,
|
||||
bucketHeight: calculateViewportHeightByNumberOfAsset(d.count, viewportWidth),
|
||||
assets: [],
|
||||
cancelToken: new AbortController()
|
||||
})),
|
||||
assets: []
|
||||
});
|
||||
};
|
||||
|
||||
const getAssetsByBucket = async (bucket: string) => {
|
||||
try {
|
||||
const currentBucketData = _assetGridState.buckets.find((b) => b.bucketDate === bucket);
|
||||
if (currentBucketData?.assets && currentBucketData.assets.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingBucketState.set({
|
||||
..._loadingBucketState,
|
||||
[bucket]: true
|
||||
});
|
||||
const { data: assets } = await api.assetApi.getAssetByTimeBucket(
|
||||
{
|
||||
timeBucket: [bucket]
|
||||
},
|
||||
{ signal: currentBucketData?.cancelToken.signal }
|
||||
);
|
||||
loadingBucketState.set({
|
||||
..._loadingBucketState,
|
||||
[bucket]: false
|
||||
});
|
||||
|
||||
// Update assetGridState with assets by time bucket
|
||||
assetGridState.update((state) => {
|
||||
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
|
||||
state.buckets[bucketIndex].assets = assets;
|
||||
state.assets = lodash.flatMap(state.buckets, (b) => b.assets);
|
||||
|
||||
return state;
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.name === 'CanceledError') {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to get asset for bucket ', bucket);
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const removeAsset = (assetId: string) => {
|
||||
assetGridState.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 = lodash.flatMap(state.buckets, (b) => b.assets);
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
const _removeBucket = (bucketDate: string) => {
|
||||
assetGridState.update((state) => {
|
||||
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate);
|
||||
state.buckets.splice(bucketIndex, 1);
|
||||
state.assets = lodash.flatMap(state.buckets, (b) => b.assets);
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
const updateBucketHeight = (bucket: string, height: number) => {
|
||||
assetGridState.update((state) => {
|
||||
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
|
||||
state.buckets[bucketIndex].bucketHeight = height;
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
const cancelBucketRequest = async (token: AbortController, bucketDate: string) => {
|
||||
token.abort();
|
||||
// set new abort controller for bucket
|
||||
assetGridState.update((state) => {
|
||||
const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate);
|
||||
state.buckets[bucketIndex].cancelToken = new AbortController();
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
setInitialState,
|
||||
getAssetsByBucket,
|
||||
removeAsset,
|
||||
updateBucketHeight,
|
||||
cancelBucketRequest
|
||||
};
|
||||
}
|
||||
|
||||
export const assetStore = createAssetStore();
|
||||
Loading…
Add table
Add a link
Reference in a new issue