import { goto, afterNavigate } from "$app/navigation"; /** * App navigation has two distinct affordances (per the Android guidelines): * * - **Up** — move to the current screen's *logical parent* in the app * hierarchy (e.g. `/library/music/albums` → `/library/music`). Deterministic, * derived from the route, and never depends on how the user got here. This is * what the in-app header arrows should do almost everywhere. * * - **Back** — pop the *actual* history stack: return to wherever the user came * from, which may be a sibling branch (a detail page reached from search vs. * from the library) or even outside the app. This is the hardware/gesture * back button's job; use it in-app only where "return to origin" is genuinely * better than Up (e.g. a detail page with many entry points). * * The old single `navigateBack` conflated the two: it called `history.back()` * first and only fell back to a path. On resume-from-background the WebView can * restore a history stack whose `length` is still > 1 but which cannot actually * go back within the app — so `history.back()` no-ops and the user is trapped on * the page. Splitting Up (pure `goto`) from Back (tracked in-app depth) removes * that trap: Up can never get stuck, and Back only fires when we *know* there is * an in-app entry to return to. */ // In-app navigation depth, maintained via the public `afterNavigate` hook rather // than reading SvelteKit's internal history-state key. Starts at 0 (the entry // page). Each forward in-app navigation increments it; a popstate (back/forward // gesture) sets it to the delta-adjusted value. When it is > 0 we know a real // in-app Back exists and won't strand the user — independent of the WebView's // possibly-stale `history.length` after a background/restore. let inAppDepth = 0; let navHookRegistered = false; /** * Register the navigation-depth tracker. Call once from the root layout's * component init (afterNavigate must run in a component context). Safe to call * more than once — only the first registration takes effect. */ export function registerNavigationTracking(): void { if (navHookRegistered) return; navHookRegistered = true; afterNavigate((nav) => { // A popstate (hardware/gesture back or forward) carries a delta; apply it so // depth tracks the true stack position. Programmatic goto/link navigations // have no delta and move one step deeper. const delta = nav.delta; if (typeof delta === "number") { inAppDepth = Math.max(0, inAppDepth + delta); } else if (nav.from) { // A real forward navigation from an existing page (not the initial load). inAppDepth += 1; } }); } /** * Reset the tracked depth. Intended for tests only, so each case starts from a * known baseline (module state persists across a test file otherwise). */ export function __resetNavigationDepthForTest(): void { inAppDepth = 0; } /** * True when there is at least one in-app history entry to pop. Unlike * `history.length > 1`, this reflects navigations that happened *within this app * session*, so a stale WebView stack after a background/restore can't fool it. */ export function canGoBack(): boolean { return inAppDepth > 0; } /** * **Up**: go to the given logical parent path. Always deterministic; never * consults history, so it cannot trap the user. Prefer this for header arrows. */ export function navigateUp(parentPath: string): void { goto(parentPath); } /** * **Back**: return to the previous in-app page when there is one, otherwise fall * back to `fallbackPath` (typically the logical parent) so the user is never * stranded. Use only where returning to the exact origin is preferable to Up * (e.g. a detail page reachable from multiple branches). */ export function navigateBack(fallbackPath: string): void { if (canGoBack()) { history.back(); } else { goto(fallbackPath); } }