mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix: back/forward navigation won't reset scroll in timeline
Fixes a bug where navigating to/from the asser-viewer from timeline causes the scroll position to be reset.
This commit is contained in:
parent
cc1cd299f3
commit
5e0118df01
1 changed files with 61 additions and 48 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/state';
|
||||||
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
||||||
import Scrubber from '$lib/components/timeline/Scrubber.svelte';
|
import Scrubber from '$lib/components/timeline/Scrubber.svelte';
|
||||||
import TimelineAssetViewer from '$lib/components/timeline/TimelineAssetViewer.svelte';
|
import TimelineAssetViewer from '$lib/components/timeline/TimelineAssetViewer.svelte';
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||||
import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
|
import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
@ -135,15 +135,6 @@
|
||||||
|
|
||||||
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId);
|
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId);
|
||||||
|
|
||||||
const assetIsVisible = (assetTop: number): boolean => {
|
|
||||||
if (!scrollableElement) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { clientHeight, scrollTop } = scrollableElement;
|
|
||||||
return assetTop >= scrollTop && assetTop < scrollTop + clientHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollToAssetId = async (assetId: string) => {
|
const scrollToAssetId = async (assetId: string) => {
|
||||||
const monthGroup = await timelineManager.findMonthGroupForAsset(assetId);
|
const monthGroup = await timelineManager.findMonthGroupForAsset(assetId);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
|
|
@ -152,11 +143,6 @@
|
||||||
|
|
||||||
const height = getAssetHeight(assetId, monthGroup);
|
const height = getAssetHeight(assetId, monthGroup);
|
||||||
|
|
||||||
// If the asset is already visible, then don't scroll.
|
|
||||||
if (assetIsVisible(height)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
timelineManager.scrollTo(height);
|
timelineManager.scrollTo(height);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
@ -171,7 +157,17 @@
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeNav = async () => {
|
export const scrollAfterNavigate = async ({ scrollToAssetQueryParam }: { scrollToAssetQueryParam: boolean }) => {
|
||||||
|
if (timelineManager.viewportHeight === 0 || timelineManager.viewportWidth === 0) {
|
||||||
|
// this can happen if you do the following navigation order
|
||||||
|
// /photos?at=<id>, /photos/<id>, http://example.com, browser back, browser back
|
||||||
|
const rect = scrollableElement?.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
timelineManager.viewportHeight = rect.height;
|
||||||
|
timelineManager.viewportWidth = rect.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scrollToAssetQueryParam) {
|
||||||
const scrollTarget = $gridScrollTarget?.at;
|
const scrollTarget = $gridScrollTarget?.at;
|
||||||
let scrolled = false;
|
let scrolled = false;
|
||||||
if (scrollTarget) {
|
if (scrollTarget) {
|
||||||
|
|
@ -179,40 +175,57 @@
|
||||||
}
|
}
|
||||||
if (!scrolled) {
|
if (!scrolled) {
|
||||||
// if the asset is not found, scroll to the top
|
// if the asset is not found, scroll to the top
|
||||||
scrollToTop();
|
scrollToTop(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
invisible = false;
|
invisible = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeNavigate(() => (timelineManager.suspendTransitions = true));
|
beforeNavigate(({ from, to }) => {
|
||||||
|
timelineManager.suspendTransitions = true;
|
||||||
afterNavigate((nav) => {
|
hasNavigatedToOrFromAssetViewer = isAssetViewerRoute(to) || isAssetViewerRoute(from);
|
||||||
const { complete } = nav;
|
|
||||||
complete.then(completeNav, completeNav);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAfterUpdate = (payload: UpdatePayload) => {
|
// tri-state boolean
|
||||||
const timelineUpdate = payload.updates.some(
|
let initialLoadWasAssetViewer: boolean | null = null;
|
||||||
(update) => update.path.endsWith('Timeline.svelte') || update.path.endsWith('assets-store.ts'),
|
let hasNavigatedToOrFromAssetViewer: boolean = false;
|
||||||
);
|
let timelineScrollPositionInitialized = false;
|
||||||
|
|
||||||
if (timelineUpdate) {
|
const completeAfterNavigate = () => {
|
||||||
setTimeout(() => {
|
const assetViewerPage = !!(page.route.id?.endsWith('/[[assetId=id]]') && page.params.assetId);
|
||||||
const asset = $page.url.searchParams.get('at');
|
let isInitial = false;
|
||||||
if (asset) {
|
// Set initial load state only once
|
||||||
$gridScrollTarget = { at: asset };
|
if (initialLoadWasAssetViewer === null) {
|
||||||
void navigate(
|
initialLoadWasAssetViewer = assetViewerPage && !hasNavigatedToOrFromAssetViewer;
|
||||||
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget },
|
isInitial = true;
|
||||||
{ replaceState: true, forceNavigate: true },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
scrollToTop();
|
|
||||||
}
|
}
|
||||||
invisible = false;
|
let scrollToAssetQueryParam = false;
|
||||||
}, 500);
|
if (
|
||||||
|
!timelineScrollPositionInitialized &&
|
||||||
|
((isInitial && !assetViewerPage) || // Direct timeline load
|
||||||
|
(!isInitial && hasNavigatedToOrFromAssetViewer)) // Navigated from asset viewer
|
||||||
|
) {
|
||||||
|
scrollToAssetQueryParam = true;
|
||||||
|
timelineScrollPositionInitialized = true;
|
||||||
}
|
}
|
||||||
|
return scrollAfterNavigate({ scrollToAssetQueryParam });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
afterNavigate(({ complete }) => void complete.then(completeAfterNavigate, completeAfterNavigate));
|
||||||
|
|
||||||
|
const handleAfterUpdate = () => {
|
||||||
|
const asset = page.url.searchParams.get('at');
|
||||||
|
if (asset) {
|
||||||
|
$gridScrollTarget = { at: asset };
|
||||||
|
}
|
||||||
|
void scrollAfterNavigate({ scrollToAssetQueryParam: true });
|
||||||
|
};
|
||||||
|
const handleBeforeUpdate = (payload: UpdatePayload) => {
|
||||||
|
const timelineUpdate = payload.updates.some((update) => update.path.endsWith('Timeline.svelte'));
|
||||||
|
if (timelineUpdate) {
|
||||||
|
timelineManager.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
const updateIsScrolling = () => (timelineManager.scrolling = true);
|
const updateIsScrolling = () => (timelineManager.scrolling = true);
|
||||||
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
|
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
|
||||||
|
|
||||||
|
|
@ -497,7 +510,7 @@
|
||||||
|
|
||||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||||
|
|
||||||
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={() => timelineManager.destroy()} />
|
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={handleBeforeUpdate} />
|
||||||
|
|
||||||
<TimelineKeyboardActions
|
<TimelineKeyboardActions
|
||||||
scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
|
scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue