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