feat: timeline performance (#16446)
* Squash - feature complete * remove need to init assetstore * More optimizations. No need to init. Fix tests * lint * add missing selector for e2e * e2e selectors again * Update: fully reactive store, some transitions, bugfixes * merge fallout * Test fallout * safari quirk * security * lint * lint * Bug fixes * lint/format * accidental commit * lock * null check, more throttle * revert long duration * Fix intersection bounds * Fix bugs in intersection calculation * lint, tweak scrubber ui a tiny bit * bugfix - deselecting asset doesnt work * fix not loading bucket, scroll off-by-1 error, jsdoc, naming
This commit is contained in:
+148
-130
@@ -100,7 +100,7 @@
|
||||
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
||||
|
||||
let backUrl: string = $state(AppRoute.ALBUMS);
|
||||
let viewMode = $state(AlbumPageViewMode.VIEW);
|
||||
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
|
||||
let isCreatingSharedAlbum = $state(false);
|
||||
let isShowActivity = $state(false);
|
||||
let isLiked: ActivityResponseDto | null = $state(null);
|
||||
@@ -203,7 +203,9 @@
|
||||
|
||||
const handleStartSlideshow = async () => {
|
||||
const asset =
|
||||
$slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0];
|
||||
$slideshowNavigation === SlideshowNavigation.Shuffle
|
||||
? await assetStore.getRandomAsset()
|
||||
: assetStore.buckets[0]?.dateGroups[0]?.intersetingAssets[0]?.asset;
|
||||
if (asset) {
|
||||
setAsset(asset);
|
||||
$slideshowState = SlideshowState.PlaySlideshow;
|
||||
@@ -211,6 +213,7 @@
|
||||
};
|
||||
|
||||
const handleEscape = async () => {
|
||||
assetStore.suspendTransitions = true;
|
||||
if (viewMode === AlbumPageViewMode.SELECT_USERS) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
@@ -270,11 +273,8 @@
|
||||
};
|
||||
|
||||
const setModeToView = async () => {
|
||||
assetStore.suspendTransitions = true;
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
assetStore.destroy();
|
||||
assetStore = new AssetStore({ albumId, order: albumOrder });
|
||||
timelineStore.destroy();
|
||||
timelineStore = new AssetStore({ isArchived: false }, albumId);
|
||||
await navigate(
|
||||
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: oldAt?.at } },
|
||||
{ replaceState: true, forceNavigate: true },
|
||||
@@ -394,14 +394,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
timelineStore.destroy();
|
||||
});
|
||||
|
||||
let album = $state(data.album);
|
||||
let albumId = $derived(album.id);
|
||||
let albumKey = $derived(`${albumId}_${albumOrder}`);
|
||||
|
||||
$effect(() => {
|
||||
if (!album.isActivityEnabled && $numberOfComments === 0) {
|
||||
@@ -409,8 +403,18 @@
|
||||
}
|
||||
});
|
||||
|
||||
let assetStore = $derived(new AssetStore({ albumId, order: albumOrder }));
|
||||
let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId));
|
||||
let assetStore = new AssetStore();
|
||||
$effect(() => {
|
||||
if (viewMode === AlbumPageViewMode.VIEW) {
|
||||
void assetStore.updateOptions({ albumId, order: albumOrder });
|
||||
} else if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
|
||||
void assetStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId });
|
||||
}
|
||||
});
|
||||
onDestroy(() => assetStore.destroy());
|
||||
// let timelineStore = new AssetStore();
|
||||
// $effect(() => void timelineStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId }));
|
||||
// onDestroy(() => timelineStore.destroy());
|
||||
|
||||
let isOwned = $derived($user.id == album.ownerId);
|
||||
|
||||
@@ -429,6 +433,22 @@
|
||||
handlePromiseError(getNumberOfComments());
|
||||
}
|
||||
});
|
||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
|
||||
const isSelectionMode = $derived(
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||
);
|
||||
const singleSelect = $derived(
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : viewMode === AlbumPageViewMode.SELECT_THUMBNAIL,
|
||||
);
|
||||
const showArchiveIcon = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS);
|
||||
const onSelect = ({ id }: { id: string }) => {
|
||||
if (viewMode !== AlbumPageViewMode.SELECT_ASSETS) {
|
||||
void handleUpdateThumbnail(id);
|
||||
}
|
||||
};
|
||||
const currentAssetIntersection = $derived(
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? timelineInteraction : assetInteraction,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
|
||||
@@ -445,7 +465,14 @@
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
{#if assetInteraction.isAllUserOwned}
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) =>
|
||||
assetStore.updateAssetOperation(ids, (asset) => {
|
||||
asset.isFavorite = isFavorite;
|
||||
return { remove: false };
|
||||
})}
|
||||
></FavoriteAction>
|
||||
{/if}
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem filename="{album.albumName}.zip" />
|
||||
@@ -482,6 +509,7 @@
|
||||
<CircleIconButton
|
||||
title={$t('add_photos')}
|
||||
onclick={async () => {
|
||||
assetStore.suspendTransitions = true;
|
||||
viewMode = AlbumPageViewMode.SELECT_ASSETS;
|
||||
oldAt = { at: $gridScrollTarget?.at };
|
||||
await navigate(
|
||||
@@ -576,127 +604,117 @@
|
||||
{/if}
|
||||
|
||||
<main class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg">
|
||||
<!-- Use key because AssetGrid can't deal with changing stores -->
|
||||
{#key albumKey}
|
||||
{#if viewMode === AlbumPageViewMode.SELECT_ASSETS}
|
||||
<AssetGrid
|
||||
enableRouting={false}
|
||||
assetStore={timelineStore}
|
||||
assetInteraction={timelineInteraction}
|
||||
isSelectionMode={true}
|
||||
/>
|
||||
{:else}
|
||||
<AssetGrid
|
||||
enableRouting={true}
|
||||
{album}
|
||||
{assetStore}
|
||||
{assetInteraction}
|
||||
isShared={album.albumUsers.length > 0}
|
||||
isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
showArchiveIcon
|
||||
onSelect={({ id }) => handleUpdateThumbnail(id)}
|
||||
onEscape={handleEscape}
|
||||
>
|
||||
{#if viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
<!-- ALBUM TITLE -->
|
||||
<section class="pt-8 md:pt-24">
|
||||
<AlbumTitle
|
||||
id={album.id}
|
||||
albumName={album.albumName}
|
||||
{isOwned}
|
||||
onUpdate={(albumName) => (album.albumName = albumName)}
|
||||
/>
|
||||
<AssetGrid
|
||||
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
|
||||
{album}
|
||||
{assetStore}
|
||||
assetInteraction={currentAssetIntersection}
|
||||
{isShared}
|
||||
{isSelectionMode}
|
||||
{singleSelect}
|
||||
{showArchiveIcon}
|
||||
{onSelect}
|
||||
onEscape={handleEscape}
|
||||
>
|
||||
{#if viewMode !== AlbumPageViewMode.SELECT_ASSETS}
|
||||
{#if viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL}
|
||||
<!-- ALBUM TITLE -->
|
||||
<section class="pt-8 md:pt-24">
|
||||
<AlbumTitle
|
||||
id={album.id}
|
||||
albumName={album.albumName}
|
||||
{isOwned}
|
||||
onUpdate={(albumName) => (album.albumName = albumName)}
|
||||
/>
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<AlbumSummary {album} />
|
||||
{/if}
|
||||
{#if album.assetCount > 0}
|
||||
<AlbumSummary {album} />
|
||||
{/if}
|
||||
|
||||
<!-- ALBUM SHARING -->
|
||||
{#if album.albumUsers.length > 0 || (album.hasSharedLink && isOwned)}
|
||||
<div class="my-3 flex gap-x-1">
|
||||
<!-- link -->
|
||||
{#if album.hasSharedLink && isOwned}
|
||||
<CircleIconButton
|
||||
title={$t('create_link_to_share')}
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiLink}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
|
||||
/>
|
||||
{/if}
|
||||
<!-- ALBUM SHARING -->
|
||||
{#if album.albumUsers.length > 0 || (album.hasSharedLink && isOwned)}
|
||||
<div class="my-3 flex gap-x-1">
|
||||
<!-- link -->
|
||||
{#if album.hasSharedLink && isOwned}
|
||||
<CircleIconButton
|
||||
title={$t('create_link_to_share')}
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiLink}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- owner -->
|
||||
<button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</button>
|
||||
|
||||
<!-- users with write access (collaborators) -->
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
||||
<button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
|
||||
<UserAvatar {user} size="md" />
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- display ellipsis if there are readonly users too -->
|
||||
{#if albumHasViewers}
|
||||
<CircleIconButton
|
||||
title={$t('view_all_users')}
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiDotsVertical}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isOwned}
|
||||
<CircleIconButton
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiPlus}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
title={$t('add_more_users')}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- ALBUM DESCRIPTION -->
|
||||
<AlbumDescription id={album.id} bind:description={album.description} {isOwned} />
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if album.assetCount === 0}
|
||||
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
|
||||
<div class="w-[300px]">
|
||||
<p class="text-xs dark:text-immich-dark-fg">{$t('add_photos').toUpperCase()}</p>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_ASSETS)}
|
||||
class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
>
|
||||
<span class="text-text-immich-primary dark:text-immich-dark-primary"
|
||||
><Icon path={mdiPlus} size="24" />
|
||||
</span>
|
||||
<span class="text-lg">{$t('select_photos')}</span>
|
||||
<!-- owner -->
|
||||
<button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</AssetGrid>
|
||||
{/if}
|
||||
|
||||
{#if showActivityStatus}
|
||||
<div class="absolute z-[2] bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||
<ActivityStatus
|
||||
disabled={!album.isActivityEnabled}
|
||||
{isLiked}
|
||||
numberOfComments={$numberOfComments}
|
||||
onFavorite={handleFavorite}
|
||||
onOpenActivityTab={handleOpenAndCloseActivityTab}
|
||||
/>
|
||||
</div>
|
||||
<!-- users with write access (collaborators) -->
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
||||
<button type="button" onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}>
|
||||
<UserAvatar {user} size="md" />
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- display ellipsis if there are readonly users too -->
|
||||
{#if albumHasViewers}
|
||||
<CircleIconButton
|
||||
title={$t('view_all_users')}
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiDotsVertical}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.VIEW_USERS)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isOwned}
|
||||
<CircleIconButton
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiPlus}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
title={$t('add_more_users')}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- ALBUM DESCRIPTION -->
|
||||
<AlbumDescription id={album.id} bind:description={album.description} {isOwned} />
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if album.assetCount === 0}
|
||||
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
|
||||
<div class="w-[300px]">
|
||||
<p class="text-xs dark:text-immich-dark-fg">{$t('add_photos').toUpperCase()}</p>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_ASSETS)}
|
||||
class="mt-5 flex w-full place-items-center gap-6 rounded-md border bg-immich-bg px-8 py-8 text-immich-fg transition-all hover:bg-gray-100 hover:text-immich-primary dark:border-none dark:bg-immich-dark-gray dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
>
|
||||
<span class="text-text-immich-primary dark:text-immich-dark-primary"
|
||||
><Icon path={mdiPlus} size="24" />
|
||||
</span>
|
||||
<span class="text-lg">{$t('select_photos')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
{/key}
|
||||
</AssetGrid>
|
||||
|
||||
{#if showActivityStatus}
|
||||
<div class="absolute z-[2] bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||
<ActivityStatus
|
||||
disabled={!album.isActivityEnabled}
|
||||
{isLiked}
|
||||
numberOfComments={$numberOfComments}
|
||||
onFavorite={handleFavorite}
|
||||
onOpenActivityTab={handleOpenAndCloseActivityTab}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
{#if album.albumUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer}
|
||||
|
||||
@@ -12,20 +12,23 @@
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
|
||||
import type { PageData } from './$types';
|
||||
import { mdiPlus, mdiDotsVertical } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
const assetStore = new AssetStore();
|
||||
void assetStore.updateOptions({ isArchived: true });
|
||||
onDestroy(() => assetStore.destroy());
|
||||
|
||||
const assetStore = new AssetStore({ isArchived: true });
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const handleEscape = () => {
|
||||
@@ -34,10 +37,6 @@
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if assetInteraction.selectionActive}
|
||||
@@ -45,14 +44,28 @@
|
||||
assets={assetInteraction.selectedAssets}
|
||||
clearSelect={() => assetInteraction.clearMultiselect()}
|
||||
>
|
||||
<ArchiveAction unarchive onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
<ArchiveAction
|
||||
unarchive
|
||||
onArchive={(ids, isArchived) =>
|
||||
assetStore.updateAssetOperation(ids, (asset) => {
|
||||
asset.isArchived = isArchived;
|
||||
return { remove: false };
|
||||
})}
|
||||
/>
|
||||
<CreateSharedLink />
|
||||
<SelectAllAssets {assetStore} {assetInteraction} />
|
||||
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) =>
|
||||
assetStore.updateAssetOperation(ids, (asset) => {
|
||||
asset.isFavorite = isFavorite;
|
||||
return { remove: false };
|
||||
})}
|
||||
/>
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const assetStore = new AssetStore({ isFavorite: true });
|
||||
const assetStore = new AssetStore();
|
||||
void assetStore.updateOptions({ isFavorite: true });
|
||||
onDestroy(() => assetStore.destroy());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const handleEscape = () => {
|
||||
@@ -38,10 +41,6 @@
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Multiselection mode app bar -->
|
||||
|
||||
@@ -98,7 +98,19 @@
|
||||
<AddToAlbum onAddToAlbum={() => cancelMultiselect(assetInteraction)} />
|
||||
<AddToAlbum onAddToAlbum={() => cancelMultiselect(assetInteraction)} shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={triggerAssetUpdate} />
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) => {
|
||||
if (data.pathAssets && data.pathAssets.length > 0) {
|
||||
for (const id of ids) {
|
||||
const asset = data.pathAssets.find((asset) => asset.id === id);
|
||||
if (asset) {
|
||||
asset.isFavorite = isFavorite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
async function navigateRandom() {
|
||||
if (viewingAssets.length <= 0) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
const index = Math.floor(Math.random() * viewingAssets.length);
|
||||
const asset = await setAssetId(viewingAssets[index]);
|
||||
|
||||
+3
-5
@@ -21,7 +21,9 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true });
|
||||
const assetStore = new AssetStore();
|
||||
$effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true }));
|
||||
onDestroy(() => assetStore.destroy());
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const handleEscape = () => {
|
||||
@@ -30,10 +32,6 @@
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="grid h-screen bg-immich-bg pt-18 dark:bg-immich-dark-bg">
|
||||
|
||||
@@ -456,10 +456,10 @@
|
||||
</UserPageLayout>
|
||||
|
||||
{#if selectHidden}
|
||||
<div
|
||||
<dialog
|
||||
open
|
||||
transition:fly={{ y: innerHeight, duration: 150, easing: quintOut, opacity: 0 }}
|
||||
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="manage-visibility-title"
|
||||
use:focusTrap
|
||||
@@ -471,5 +471,5 @@
|
||||
onClose={() => (selectHidden = false)}
|
||||
{loadNextPage}
|
||||
/>
|
||||
</div>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
+11
-12
@@ -74,14 +74,9 @@
|
||||
let numberOfAssets = $state(data.statistics.assets);
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
const assetStoreOptions = { isArchived: false, personId: data.person.id };
|
||||
const assetStore = new AssetStore(assetStoreOptions);
|
||||
|
||||
$effect(() => {
|
||||
// Check to trigger rebuild the timeline when navigating between people from the info panel
|
||||
assetStoreOptions.personId = data.person.id;
|
||||
handlePromiseError(assetStore.updateOptions(assetStoreOptions));
|
||||
});
|
||||
const assetStore = new AssetStore();
|
||||
$effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id }));
|
||||
onDestroy(() => assetStore.destroy());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
@@ -360,9 +355,6 @@
|
||||
await updateAssetCount();
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
let person = $derived(data.person);
|
||||
|
||||
let thumbnailData = $derived(getPeopleThumbnailUrl(person));
|
||||
@@ -418,7 +410,14 @@
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) =>
|
||||
assetStore.updateAssetOperation(ids, (asset) => {
|
||||
asset.isFavorite = isFavorite;
|
||||
return { remove: false };
|
||||
})}
|
||||
/>
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
|
||||
<MenuOption
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true });
|
||||
const assetStore = new AssetStore();
|
||||
void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
|
||||
onDestroy(() => assetStore.destroy());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
let selectedAssets = $derived(assetInteraction.selectedAssetsArray);
|
||||
@@ -67,10 +70,6 @@
|
||||
assetStore.updateAssets([still]);
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
|
||||
beforeNavigate(() => {
|
||||
isFaceEditMode.value = false;
|
||||
});
|
||||
@@ -88,7 +87,14 @@
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) =>
|
||||
assetStore.updateAssetOperation(ids, (asset) => {
|
||||
asset.isFavorite = isFavorite;
|
||||
return { remove: false };
|
||||
})}
|
||||
></FavoriteAction>
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
{#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected}
|
||||
|
||||
@@ -136,13 +136,17 @@
|
||||
nextPage = 1;
|
||||
searchResultAssets = [];
|
||||
searchResultAlbums = [];
|
||||
await loadNextPage();
|
||||
await loadNextPage(true);
|
||||
}
|
||||
|
||||
const loadNextPage = async () => {
|
||||
// eslint-disable-next-line svelte/valid-prop-names-in-kit-pages
|
||||
export const loadNextPage = async (force?: boolean) => {
|
||||
if (!nextPage || searchResultAssets.length >= MAX_ASSET_COUNT) {
|
||||
return;
|
||||
}
|
||||
if (isLoading && !force) {
|
||||
return;
|
||||
}
|
||||
isLoading = true;
|
||||
|
||||
const searchDto: SearchTerms = {
|
||||
@@ -232,9 +236,6 @@
|
||||
return tagNames.join(', ');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-self-assign
|
||||
const triggerAssetUpdate = () => (searchResultAssets = searchResultAssets);
|
||||
|
||||
const onAddToAlbum = (assetIds: string[]) => {
|
||||
if (terms.isNotInAlbum.toString() == 'true') {
|
||||
const assetIdSet = new Set(assetIds);
|
||||
@@ -262,13 +263,23 @@
|
||||
<AddToAlbum {onAddToAlbum} />
|
||||
<AddToAlbum shared {onAddToAlbum} />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={triggerAssetUpdate} />
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={(ids, isFavorite) => {
|
||||
for (const id of ids) {
|
||||
const asset = searchResultAssets.find((asset) => asset.id === id);
|
||||
if (asset) {
|
||||
asset.isFavorite = isFavorite;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />
|
||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
|
||||
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
||||
<TagAction menuItem />
|
||||
{/if}
|
||||
@@ -281,6 +292,10 @@
|
||||
{:else}
|
||||
<div class="fixed z-[100] top-0 left-0 w-full">
|
||||
<ControlAppBar onClose={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
||||
<div
|
||||
class="-z-[1] bg-immich-bg dark:bg-immich-dark-bg"
|
||||
style="position:absolute;top:0;left:0;right:0;bottom:0;"
|
||||
></div>
|
||||
<div class="w-full flex-1 pl-4">
|
||||
<SearchBar grayTheme={false} value={terms?.query ?? ''} searchQuery={terms} />
|
||||
</div>
|
||||
@@ -329,45 +344,43 @@
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="relative mb-12 bg-immich-bg dark:bg-immich-dark-bg m-4"
|
||||
class="mb-12 bg-immich-bg dark:bg-immich-dark-bg m-4"
|
||||
bind:clientHeight={viewport.height}
|
||||
bind:clientWidth={viewport.width}
|
||||
>
|
||||
<section class="immich-scrollbar relative overflow-y-auto">
|
||||
{#if searchResultAlbums.length > 0}
|
||||
<section>
|
||||
<div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">{$t('albums').toUpperCase()}</div>
|
||||
<AlbumCardGroup albums={searchResultAlbums} showDateRange showItemCount />
|
||||
{#if searchResultAlbums.length > 0}
|
||||
<section>
|
||||
<div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">{$t('albums').toUpperCase()}</div>
|
||||
<AlbumCardGroup albums={searchResultAlbums} showDateRange showItemCount />
|
||||
|
||||
<div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80">
|
||||
{$t('photos_and_videos').toUpperCase()}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if searchResultAssets.length > 0}
|
||||
<GalleryViewer
|
||||
assets={searchResultAssets}
|
||||
{assetInteraction}
|
||||
onIntersected={loadNextPage}
|
||||
showArchiveIcon={true}
|
||||
{viewport}
|
||||
/>
|
||||
{:else if !isLoading}
|
||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<Icon path={mdiImageOffOutline} size="3.5em" />
|
||||
<p class="mt-5 text-3xl font-medium">{$t('no_results')}</p>
|
||||
<p class="text-base font-normal">{$t('no_results_description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex justify-center py-16 items-center">
|
||||
<LoadingSpinner size="48" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80">
|
||||
{$t('photos_and_videos').toUpperCase()}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
<section id="search-content">
|
||||
{#if searchResultAssets.length > 0}
|
||||
<GalleryViewer
|
||||
assets={searchResultAssets}
|
||||
{assetInteraction}
|
||||
onIntersected={loadNextPage}
|
||||
showArchiveIcon={true}
|
||||
{viewport}
|
||||
/>
|
||||
{:else if !isLoading}
|
||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
<Icon path={mdiImageOffOutline} size="3.5em" />
|
||||
<p class="mt-5 text-3xl font-medium">{$t('no_results')}</p>
|
||||
<p class="text-base font-normal">{$t('no_results_description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex justify-center py-16 items-center">
|
||||
<LoadingSpinner size="48" />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@@ -39,8 +40,9 @@
|
||||
const buildMap = (tags: TagResponseDto[]) => {
|
||||
return Object.fromEntries(tags.map((tag) => [tag.value, tag]));
|
||||
};
|
||||
|
||||
const assetStore = new AssetStore({});
|
||||
const assetStore = new AssetStore();
|
||||
$effect(() => void assetStore.updateOptions({ deferInit: !tag, tagId }));
|
||||
onDestroy(() => assetStore.destroy());
|
||||
|
||||
let tags = $state<TagResponseDto[]>([]);
|
||||
$effect(() => {
|
||||
@@ -52,10 +54,6 @@
|
||||
let tagId = $derived(tag?.id);
|
||||
let tree = $derived(buildTree(tags.map((tag) => tag.value)));
|
||||
|
||||
$effect.pre(() => {
|
||||
void assetStore.updateOptions({ tagId });
|
||||
});
|
||||
|
||||
const handleNavigation = async (tag: string) => {
|
||||
await navigateToView(normalizeTreePath(`${data.path || ''}/${tag}`));
|
||||
};
|
||||
|
||||
@@ -36,8 +36,10 @@
|
||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
}
|
||||
|
||||
const options = { isTrashed: true };
|
||||
const assetStore = new AssetStore(options);
|
||||
const assetStore = new AssetStore();
|
||||
void assetStore.updateOptions({ isTrashed: true });
|
||||
onDestroy(() => assetStore.destroy());
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const handleEmptyTrash = async () => {
|
||||
@@ -56,9 +58,6 @@
|
||||
message: $t('assets_permanently_deleted_count', { values: { count } }),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
// reset asset grid (TODO fix in asset store that it should reset when it is empty)
|
||||
await assetStore.updateOptions(options);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_empty_trash'));
|
||||
}
|
||||
@@ -80,7 +79,10 @@
|
||||
});
|
||||
|
||||
// reset asset grid (TODO fix in asset store that it should reset when it is empty)
|
||||
await assetStore.updateOptions(options);
|
||||
// note - this is still a problem, but updateOptions with the same value will not
|
||||
// do anything, so need to flip it for it to reload/reinit
|
||||
// await assetStore.updateOptions({ deferInit: true, isTrashed: true });
|
||||
// await assetStore.updateOptions({ deferInit: false, isTrashed: true });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_restore_trash'));
|
||||
}
|
||||
@@ -92,10 +94,6 @@
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if assetInteraction.selectionActive}
|
||||
|
||||
Reference in New Issue
Block a user