Merge d583d3d639ac3e6d84ca0564d244c69de3ed8bce into 4d20b11f256c40e3894c229ed638d7ea04ebdc44

This commit is contained in:
Calum Dingwall 2024-10-01 15:35:49 -04:00 committed by GitHub
commit 79272fba64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 237 additions and 7 deletions

View File

@ -0,0 +1,87 @@
import { navigating } from '$app/stores';
import { AppRoute, SessionStorageKey } from '$lib/constants';
import { handlePromiseError } from '$lib/utils';
interface Options {
/**
* {@link AppRoute} for subpages that scroll state should be kept while visiting.
*
* This must be kept the same in all subpages of this route for the scroll memory clearer to work.
*/
routeStartsWith: AppRoute;
/**
* Function to clear additional data/state before scrolling (ex infinite scroll).
*/
beforeClear?: () => void;
}
interface PageOptions extends Options {
/**
* Function to save additional data/state before scrolling (ex infinite scroll).
*/
beforeSave?: () => void;
/**
* Function to load additional data/state before scrolling (ex infinite scroll).
*/
beforeScroll?: () => Promise<void>;
}
/**
* @param node The scroll slot element, typically from {@link UserPageLayout}
*/
export function scrollMemory(
node: HTMLElement,
{ routeStartsWith, beforeSave, beforeClear, beforeScroll }: PageOptions,
) {
const unsubscribeNavigating = navigating.subscribe((navigation) => {
const existingScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION);
if (navigation?.to && !existingScroll) {
// Save current scroll information when going into a subpage.
if (navigation.to.url.pathname.startsWith(routeStartsWith)) {
beforeSave?.();
sessionStorage.setItem(SessionStorageKey.SCROLL_POSITION, node.scrollTop.toString());
} else {
beforeClear?.();
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
}
}
});
handlePromiseError(
(async () => {
await beforeScroll?.();
const newScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION);
if (newScroll) {
node.scroll({
top: Number.parseFloat(newScroll),
behavior: 'instant',
});
}
beforeClear?.();
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
})(),
);
return {
destroy() {
unsubscribeNavigating();
},
};
}
export function scrollMemoryClearer(_node: HTMLElement, { routeStartsWith, beforeClear }: Options) {
const unsubscribeNavigating = navigating.subscribe((navigation) => {
// Forget scroll position from main page if going somewhere else.
if (navigation?.to && !navigation?.to.url.pathname.startsWith(routeStartsWith)) {
beforeClear?.();
sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION);
}
});
return {
destroy() {
unsubscribeNavigating();
},
};
}

View File

@ -0,0 +1,67 @@
/**
* @license Apache-2.0
* https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/src/internal/useActions.ts
*/
export type SvelteActionReturnType<P> = {
update?: (newParams?: P) => void;
destroy?: () => void;
} | void;
export type SvelteHTMLActionType<P> = (node: HTMLElement, params?: P) => SvelteActionReturnType<P>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type HTMLActionEntry<P = any> = SvelteHTMLActionType<P> | [SvelteHTMLActionType<P>, P];
export type HTMLActionArray = HTMLActionEntry[];
export type SvelteSVGActionType<P> = (node: SVGElement, params?: P) => SvelteActionReturnType<P>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type SVGActionEntry<P = any> = SvelteSVGActionType<P> | [SvelteSVGActionType<P>, P];
export type SVGActionArray = SVGActionEntry[];
export type ActionArray = HTMLActionArray | SVGActionArray;
export function useActions(node: HTMLElement | SVGElement, actions: ActionArray) {
const actionReturns: SvelteActionReturnType<unknown>[] = [];
if (actions) {
for (const actionEntry of actions) {
const action = Array.isArray(actionEntry) ? actionEntry[0] : actionEntry;
if (Array.isArray(actionEntry) && actionEntry.length > 1) {
actionReturns.push(action(node as HTMLElement & SVGElement, actionEntry[1]));
} else {
actionReturns.push(action(node as HTMLElement & SVGElement));
}
}
}
return {
update(actions: ActionArray) {
if ((actions?.length || 0) != actionReturns.length) {
throw new Error('You must not change the length of an actions array.');
}
if (actions) {
for (const [i, returnEntry] of actionReturns.entries()) {
if (returnEntry && returnEntry.update) {
const actionEntry = actions[i];
if (Array.isArray(actionEntry) && actionEntry.length > 1) {
returnEntry.update(actionEntry[1]);
} else {
returnEntry.update();
}
}
}
}
},
destroy() {
for (const returnEntry of actionReturns) {
returnEntry?.destroy?.();
}
},
};
}

View File

@ -7,6 +7,7 @@
import NavigationBar from '../shared-components/navigation-bar/navigation-bar.svelte'; import NavigationBar from '../shared-components/navigation-bar/navigation-bar.svelte';
import SideBar from '../shared-components/side-bar/side-bar.svelte'; import SideBar from '../shared-components/side-bar/side-bar.svelte';
import AdminSideBar from '../shared-components/side-bar/admin-side-bar.svelte'; import AdminSideBar from '../shared-components/side-bar/admin-side-bar.svelte';
import { useActions, type ActionArray } from '$lib/actions/use-actions';
export let hideNavbar = false; export let hideNavbar = false;
export let showUploadButton = false; export let showUploadButton = false;
@ -14,6 +15,7 @@
export let description: string | undefined = undefined; export let description: string | undefined = undefined;
export let scrollbar = true; export let scrollbar = true;
export let admin = false; export let admin = false;
export let use: ActionArray = [];
$: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden'; $: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden';
$: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full'; $: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
@ -55,7 +57,7 @@
</div> </div>
{/if} {/if}
<div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto"> <div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto" use:useActions={use}>
<slot /> <slot />
</div> </div>
</section> </section>

View File

@ -83,6 +83,11 @@ export enum QueryParameter {
PATH = 'path', PATH = 'path',
} }
export enum SessionStorageKey {
INFINITE_SCROLL_PAGE = 'infiniteScrollPage',
SCROLL_POSITION = 'scrollPosition',
}
export enum OpenSettingQueryParameterValue { export enum OpenSettingQueryParameterValue {
OAUTH = 'oauth', OAUTH = 'oauth',
JOB = 'job', JOB = 'job',

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { scrollMemory } from '$lib/actions/scroll-memory';
import { AlbumFilter, albumViewSettings } from '$lib/stores/preferences.store'; import { AlbumFilter, albumViewSettings } from '$lib/stores/preferences.store';
import { createAlbumAndRedirect } from '$lib/utils/album-utils'; import { createAlbumAndRedirect } from '$lib/utils/album-utils';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
@ -8,6 +9,7 @@
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import GroupTab from '$lib/components/elements/group-tab.svelte'; import GroupTab from '$lib/components/elements/group-tab.svelte';
import SearchBar from '$lib/components/elements/search-bar.svelte'; import SearchBar from '$lib/components/elements/search-bar.svelte';
import { AppRoute } from '$lib/constants';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let data: PageData; export let data: PageData;
@ -16,7 +18,7 @@
let albumGroups: string[] = []; let albumGroups: string[] = [];
</script> </script>
<UserPageLayout title={data.meta.title}> <UserPageLayout title={data.meta.title} use={[[scrollMemory, { routeStartsWith: AppRoute.ALBUMS }]]}>
<div class="flex place-items-center gap-2" slot="buttons"> <div class="flex place-items-center gap-2" slot="buttons">
<AlbumsControls {albumGroups} bind:searchQuery /> <AlbumsControls {albumGroups} bind:searchQuery />
</div> </div>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto, onNavigate } from '$app/navigation'; import { afterNavigate, goto, onNavigate } from '$app/navigation';
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
import AlbumDescription from '$lib/components/album-page/album-description.svelte'; import AlbumDescription from '$lib/components/album-page/album-description.svelte';
import AlbumOptions from '$lib/components/album-page/album-options.svelte'; import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import AlbumSummary from '$lib/components/album-page/album-summary.svelte'; import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
@ -430,7 +431,11 @@
}); });
</script> </script>
<div class="flex overflow-hidden" bind:clientWidth={globalWidth}> <div
class="flex overflow-hidden"
bind:clientWidth={globalWidth}
use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}
>
<div class="relative w-full shrink"> <div class="relative w-full shrink">
{#if $isMultiSelectState} {#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>

View File

@ -2,6 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { focusTrap } from '$lib/actions/focus-trap'; import { focusTrap } from '$lib/actions/focus-trap';
import { scrollMemory } from '$lib/actions/scroll-memory';
import Button from '$lib/components/elements/buttons/button.svelte'; import Button from '$lib/components/elements/buttons/button.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
@ -17,7 +18,7 @@
notificationController, notificationController,
NotificationType, NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants'; import { ActionQueryParameterValue, AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { websocketEvents } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
@ -51,6 +52,7 @@
let showSetBirthDateModal = false; let showSetBirthDateModal = false;
let showMergeModal = false; let showMergeModal = false;
let personName = ''; let personName = '';
let currentPage = 1;
let nextPage = data.people.hasNextPage ? 2 : null; let nextPage = data.people.hasNextPage ? 2 : null;
let personMerge1: PersonResponseDto; let personMerge1: PersonResponseDto;
let personMerge2: PersonResponseDto; let personMerge2: PersonResponseDto;
@ -67,6 +69,7 @@
searchName = getSearchedPeople; searchName = getSearchedPeople;
handlePromiseError(handleSearchPeople(true, searchName)); handlePromiseError(handleSearchPeople(true, searchName));
} }
return websocketEvents.on('on_person_thumbnail', (personId: string) => { return websocketEvents.on('on_person_thumbnail', (personId: string) => {
for (const person of people) { for (const person of people) {
if (person.id === personId) { if (person.id === personId) {
@ -79,6 +82,36 @@
}); });
}); });
const loadInitialScroll = () =>
new Promise<void>((resolve) => {
// Load up to previously loaded page when returning.
let newNextPage = sessionStorage.getItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
if (newNextPage && nextPage) {
let startingPage = nextPage,
pagesToLoad = Number.parseInt(newNextPage) - nextPage;
if (pagesToLoad) {
handlePromiseError(
Promise.all(
Array.from({ length: pagesToLoad }).map((_, i) => {
return getAllPeople({ withHidden: true, page: startingPage + i });
}),
).then((pages) => {
for (const page of pages) {
people = people.concat(page.people);
}
currentPage = startingPage + pagesToLoad - 1;
nextPage = pages.at(-1)?.hasNextPage ? startingPage + pagesToLoad : null;
resolve(); // wait until extra pages are loaded
}),
);
} else {
resolve();
}
sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
}
});
const loadNextPage = async () => { const loadNextPage = async () => {
if (!nextPage) { if (!nextPage) {
return; return;
@ -87,6 +120,9 @@
try { try {
const { people: newPeople, hasNextPage } = await getAllPeople({ withHidden: true, page: nextPage }); const { people: newPeople, hasNextPage } = await getAllPeople({ withHidden: true, page: nextPage });
people = people.concat(newPeople); people = people.concat(newPeople);
if (nextPage !== null) {
currentPage = nextPage;
}
nextPage = hasNextPage ? nextPage + 1 : null; nextPage = hasNextPage ? nextPage + 1 : null;
} catch (error) { } catch (error) {
handleError(error, $t('errors.failed_to_load_people')); handleError(error, $t('errors.failed_to_load_people'));
@ -311,6 +347,23 @@
<UserPageLayout <UserPageLayout
title={$t('people')} title={$t('people')}
description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`} description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
use={[
[
scrollMemory,
{
routeStartsWith: AppRoute.PEOPLE,
beforeSave: () => {
if (currentPage) {
sessionStorage.setItem(SessionStorageKey.INFINITE_SCROLL_PAGE, currentPage.toString());
}
},
beforeClear: () => {
sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
},
beforeLoad: loadInitialScroll,
},
],
]}
> >
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
{#if people.length > 0} {#if people.length > 0}

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto } from '$app/navigation'; import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
@ -25,7 +26,7 @@
NotificationType, NotificationType,
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store'; import { AssetStore } from '$lib/stores/assets.store';
@ -181,7 +182,7 @@
type: NotificationType.Info, type: NotificationType.Info,
}); });
await goto(previousRoute, { replaceState: true }); await goto(previousRoute);
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_hide_person')); handleError(error, $t('errors.unable_to_hide_person'));
} }
@ -436,7 +437,15 @@
{/if} {/if}
</header> </header>
<main class="relative h-screen overflow-hidden bg-immich-bg tall:ml-4 pt-[var(--navbar-height)] dark:bg-immich-dark-bg"> <main
class="relative h-screen overflow-hidden bg-immich-bg tall:ml-4 pt-[var(--navbar-height)] dark:bg-immich-dark-bg"
use:scrollMemoryClearer={{
routeStartsWith: AppRoute.PEOPLE,
beforeClear: () => {
sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
},
}}
>
{#key person.id} {#key person.id}
<AssetGrid <AssetGrid
enableRouting={true} enableRouting={true}