1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-28 09:33:27 +02:00

refactor(web): asset store (#3528)

* refactor(web): asset store

* chore: remove TODO
This commit is contained in:
Jason Rasmussen 2023-08-03 11:44:12 -04:00 committed by GitHub
parent 01210dceac
commit 5617b57b26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 275 additions and 334 deletions

View File

@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { AssetStore } from '$lib/stores/assets.store';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import type { AssetResponseDto } from '@api'; import { TimeGroupEnum, type AssetResponseDto } from '@api';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import AssetGrid from '../photos-page/asset-grid.svelte'; import AssetGrid from '../photos-page/asset-grid.svelte';
import ControlAppBar from '../shared-components/control-app-bar.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 dispatch = createEventDispatcher();
const assetStore = createAssetStore(); const assetStore = new AssetStore({ timeGroup: TimeGroupEnum.Month });
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { selectedAssets, assetsInAlbumState } = assetInteractionStore; const { selectedAssets, assetsInAlbumState } = assetInteractionStore;

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { BucketPosition } from '$lib/models/asset-grid-state'; import { BucketPosition } from '$lib/stores/assets.store';
import { onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
export let once = false; export let once = false;
export let top = 0; export let top = 0;

View File

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { get } from 'svelte/store';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { BucketPosition, type AssetStore } from '$lib/stores/assets.store';
import { handleError } from '$lib/utils/handle-error';
import SelectAll from 'svelte-material-icons/SelectAll.svelte'; import SelectAll from 'svelte-material-icons/SelectAll.svelte';
import TimerSand from 'svelte-material-icons/TimerSand.svelte'; import TimerSand from 'svelte-material-icons/TimerSand.svelte';
import { handleError } from '../../../utils/handle-error'; import { get } from 'svelte/store';
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 assetStore: AssetStore;
export let assetInteractionStore: AssetInteractionStore; export let assetInteractionStore: AssetInteractionStore;

View File

@ -13,12 +13,13 @@
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import type { AssetStore } from '$lib/stores/assets.store'; import type { AssetStore } from '$lib/stores/assets.store';
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import type { Viewport } from '$lib/stores/assets.store';
export let assets: AssetResponseDto[]; export let assets: AssetResponseDto[];
export let bucketDate: string; export let bucketDate: string;
export let bucketHeight: number; export let bucketHeight: number;
export let isAlbumSelectionMode = false; export let isAlbumSelectionMode = false;
export let viewportWidth: number; export let viewport: Viewport;
export let assetStore: AssetStore; export let assetStore: AssetStore;
export let assetInteractionStore: AssetInteractionStore; export let assetInteractionStore: AssetInteractionStore;
@ -45,7 +46,7 @@
for (let group of assetsGroupByDate) { for (let group of assetsGroupByDate) {
const justifiedLayoutResult = justifiedLayout(group.map(getAssetRatio), { const justifiedLayoutResult = justifiedLayout(group.map(getAssetRatio), {
boxSpacing: 2, boxSpacing: 2,
containerWidth: Math.floor(viewportWidth), containerWidth: Math.floor(viewport.width),
containerPadding: 0, containerPadding: 0,
targetRowHeightTolerance: 0.15, targetRowHeightTolerance: 0.15,
targetRowHeight: 235, targetRowHeight: 235,
@ -59,7 +60,7 @@
})(); })();
$: { $: {
if (actualBucketHeight && actualBucketHeight != 0 && actualBucketHeight != bucketHeight) { if (actualBucketHeight && actualBucketHeight !== 0 && actualBucketHeight != bucketHeight) {
const heightDelta = assetStore.updateBucket(bucketDate, actualBucketHeight); const heightDelta = assetStore.updateBucket(bucketDate, actualBucketHeight);
if (heightDelta !== 0) { if (heightDelta !== 0) {
scrollTimeline(heightDelta); scrollTimeline(heightDelta);
@ -143,12 +144,7 @@
}; };
</script> </script>
<section <section id="asset-group-by-date" class="flex flex-wrap gap-x-12" bind:clientHeight={actualBucketHeight}>
id="asset-group-by-date"
class="flex flex-wrap gap-x-12"
bind:clientHeight={actualBucketHeight}
bind:clientWidth={viewportWidth}
>
{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)} {#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
{@const dateGroupTitle = formatGroupTitle(DateTime.fromISO(assetsInDateGroup[0].fileCreatedAt).startOf('day'))} {@const dateGroupTitle = formatGroupTitle(DateTime.fromISO(assetsInDateGroup[0].fileCreatedAt).startOf('day'))}
<!-- Asset Group By Date --> <!-- Asset Group By Date -->

View File

@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import { BucketPosition } from '$lib/models/asset-grid-state';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
import type { UserResponseDto } from '@api'; import type { AssetResponseDto } from '@api';
import { AssetResponseDto, TimeGroupEnum, api } from '@api';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import AssetViewer from '../asset-viewer/asset-viewer.svelte'; import AssetViewer from '../asset-viewer/asset-viewer.svelte';
@ -21,11 +19,10 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import type { AssetStore } from '$lib/stores/assets.store'; import { BucketPosition, type AssetStore, type Viewport } from '$lib/stores/assets.store';
import { isSearchEnabled } from '$lib/stores/search.store'; import { isSearchEnabled } from '$lib/stores/search.store';
import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
export let user: UserResponseDto | undefined = undefined;
export let isAlbumSelectionMode = false; export let isAlbumSelectionMode = false;
export let showMemoryLane = false; export let showMemoryLane = false;
@ -36,8 +33,7 @@
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore; let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore;
let viewportHeight = 0; const viewport: Viewport = { width: 0, height: 0 };
let viewportWidth = 0;
let assetGridElement: HTMLElement; let assetGridElement: HTMLElement;
let showShortcuts = false; let showShortcuts = false;
@ -45,23 +41,13 @@
onMount(async () => { onMount(async () => {
document.addEventListener('keydown', onKeyboardPress); document.addEventListener('keydown', onKeyboardPress);
const { data: timeBuckets } = await api.assetApi.getAssetCountByTimeBucket({ await assetStore.init(viewport);
getAssetCountByTimeBucketDto: {
timeGroup: TimeGroupEnum.Month,
userId: user?.id,
withoutThumbs: true,
},
});
assetStore.init({ width: viewportHeight, height: viewportWidth }, timeBuckets.buckets, user?.id);
}); });
onDestroy(() => { onDestroy(() => {
if (browser) { if (browser) {
document.removeEventListener('keydown', onKeyboardPress); document.removeEventListener('keydown', onKeyboardPress);
} }
assetStore.init({ width: 0, height: 0 }, [], undefined);
}); });
const handleKeyboardPress = (event: KeyboardEvent) => { const handleKeyboardPress = (event: KeyboardEvent) => {
@ -292,10 +278,10 @@
<ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} /> <ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} />
{/if} {/if}
{#if viewportHeight && $assetStore.initialized && $assetStore.timelineHeight > viewportHeight} {#if $assetStore.timelineHeight > viewport.height}
<Scrollbar <Scrollbar
{assetStore} {assetStore}
scrollbarHeight={viewportHeight} scrollbarHeight={viewport.height}
scrollTop={lastScrollPosition} scrollTop={lastScrollPosition}
on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)} on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)}
on:onscrollbardrag={(e) => handleScrollbarDrag(e.detail)} on:onscrollbardrag={(e) => handleScrollbarDrag(e.detail)}
@ -306,8 +292,8 @@
<section <section
id="asset-grid" id="asset-grid"
class="scrollbar-hidden mb-4 ml-4 mr-[60px] overflow-y-auto" class="scrollbar-hidden mb-4 ml-4 mr-[60px] overflow-y-auto"
bind:clientHeight={viewportHeight} bind:clientHeight={viewport.height}
bind:clientWidth={viewportWidth} bind:clientWidth={viewport.width}
bind:this={assetGridElement} bind:this={assetGridElement}
on:scroll={handleTimelineScroll} on:scroll={handleTimelineScroll}
> >
@ -337,7 +323,7 @@
assets={bucket.assets} assets={bucket.assets}
bucketDate={bucket.bucketDate} bucketDate={bucket.bucketDate}
bucketHeight={bucket.bucketHeight} bucketHeight={bucket.bucketHeight}
{viewportWidth} {viewport}
/> />
{/if} {/if}
</div> </div>

View File

@ -1,248 +0,0 @@
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',
Below = 'below',
Visible = 'visible',
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
* This value is first estimated by the number of asset and later is corrected as the user scroll
*/
bucketHeight!: number;
bucketDate!: string;
assets!: AssetResponseDto[];
cancelToken!: AbortController | null;
position!: BucketPosition;
}
const THUMBNAIL_HEIGHT = 235;
export class AssetGridState implements AssetStore {
private store$ = writable(this);
private assetToBucket: Record<string, AssetLookup> = {};
private viewport: Viewport = { width: 0, height: 0 };
private userId: string | undefined;
initialized = false;
timelineHeight = 0;
buckets: AssetBucket[] = [];
assets: AssetResponseDto[] = [];
subscribe = this.store$.subscribe;
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<void> {
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<string | null> {
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<string | null> {
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<string, AssetLookup> = {};
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);
}
}

View File

@ -1,38 +1,246 @@
import { AssetBucket, AssetGridState, BucketPosition, Viewport } from '$lib/models/asset-grid-state'; import { api, AssetResponseDto, GetAssetCountByTimeBucketDto } from '@api';
import type { AssetCountByTimeBucket } from '@api'; import { writable } from 'svelte/store';
import { handleError } from '../utils/handle-error';
export interface AssetStore { export enum BucketPosition {
init: (viewport: Viewport, data: AssetCountByTimeBucket[], userId: string | undefined) => void; Above = 'above',
Below = 'below',
// bucket Visible = 'visible',
loadBucket: (bucket: string, position: BucketPosition) => Promise<void>; Unknown = 'unknown',
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<string | null>;
getPreviousAssetId: (assetId: string) => Promise<string | null>;
// store
subscribe: (run: (value: AssetGridState) => void, invalidate?: (value?: AssetGridState) => void) => () => void;
} }
export function createAssetStore(): AssetStore { export type AssetStoreOptions = GetAssetCountByTimeBucketDto;
const store = new AssetGridState();
return { export interface Viewport {
init: store.init.bind(store), width: number;
loadBucket: store.loadBucket.bind(store), height: number;
updateBucket: store.updateBucket.bind(store), }
cancelBucket: store.cancelBucket.bind(store),
removeAsset: store.removeAsset.bind(store), interface AssetLookup {
updateAsset: store.updateAsset.bind(store), bucket: AssetBucket;
getNextAssetId: store.getNextAssetId.bind(store), bucketIndex: number;
getPreviousAssetId: store.getPreviousAssetId.bind(store), assetIndex: number;
subscribe: store.subscribe, }
};
export class AssetBucket {
/**
* The DOM height of the bucket in pixel
* This value is first estimated by the number of asset and later is corrected as the user scroll
*/
bucketHeight!: number;
bucketDate!: string;
assets!: AssetResponseDto[];
cancelToken!: AbortController | null;
position!: BucketPosition;
}
const THUMBNAIL_HEIGHT = 235;
export class AssetStore {
private store$ = writable(this);
private assetToBucket: Record<string, AssetLookup> = {};
timelineHeight = 0;
buckets: AssetBucket[] = [];
assets: AssetResponseDto[] = [];
constructor(private options: AssetStoreOptions) {
this.store$.set(this);
}
subscribe = this.store$.subscribe;
async init(viewport: Viewport) {
const { data } = await api.assetApi.getAssetCountByTimeBucket({
getAssetCountByTimeBucketDto: { ...this.options, withoutThumbs: true },
});
this.buckets = data.buckets.map((bucket) => {
const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10);
const rows = Math.ceil(unwrappedWidth / 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 < viewport.height) {
height += bucket.bucketHeight;
this.loadBucket(bucket.bucketDate, BucketPosition.Visible);
continue;
}
break;
}
}
async loadBucket(bucketDate: string, position: BucketPosition): Promise<void> {
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],
...this.options,
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;
}
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;
}
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<string | null> {
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<string | null> {
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<string, AssetLookup> = {};
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);
}
} }

View File

@ -8,16 +8,17 @@
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { AssetStore } from '$lib/stores/assets.store';
import { TimeGroupEnum } from '@api';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import Plus from 'svelte-material-icons/Plus.svelte'; import Plus from 'svelte-material-icons/Plus.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { createAssetStore } from '$lib/stores/assets.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
export let data: PageData; export let data: PageData;
const assetStore = createAssetStore(); const assetStore = new AssetStore({ timeGroup: TimeGroupEnum.Month, userId: data.partner.id });
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
@ -39,12 +40,12 @@
{:else} {:else}
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}> <ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<p class="text-immich-fg dark:text-immich-dark-fg"> <p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
{data.partner.firstName} {data.partner.firstName}
{data.partner.lastName}'s photos {data.partner.lastName}'s photos
</p> </p>
</svelte:fragment> </svelte:fragment>
</ControlAppBar> </ControlAppBar>
{/if} {/if}
<AssetGrid {assetStore} {assetInteractionStore} user={data.partner} /> <AssetGrid {assetStore} {assetInteractionStore} />
</main> </main>

View File

@ -11,10 +11,10 @@
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; 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 AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { createAssetStore } from '$lib/stores/assets.store'; import { AssetStore } from '$lib/stores/assets.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { api } from '@api'; import { TimeGroupEnum, api } from '@api';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
import Plus from 'svelte-material-icons/Plus.svelte'; import Plus from 'svelte-material-icons/Plus.svelte';
@ -23,7 +23,7 @@
export let data: PageData; export let data: PageData;
let assetCount = 1; let assetCount = 1;
const assetStore = createAssetStore(); const assetStore = new AssetStore({ timeGroup: TimeGroupEnum.Month });
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
@ -53,7 +53,7 @@
<AddToAlbum /> <AddToAlbum />
<AddToAlbum shared /> <AddToAlbum shared />
</AssetSelectContextMenu> </AssetSelectContextMenu>
<DeleteAssets onAssetDelete={assetStore.removeAsset} /> <DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} />
<AssetSelectContextMenu icon={DotsVertical} title="Menu"> <AssetSelectContextMenu icon={DotsVertical} title="Menu">
<FavoriteAction menuItem removeFavorite={isAllFavorite} /> <FavoriteAction menuItem removeFavorite={isAllFavorite} />
<DownloadAction menuItem /> <DownloadAction menuItem />