Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
72 lines
2.5 KiB
Svelte
72 lines
2.5 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy, setContext } from "svelte";
|
|
import { goto } from "$app/navigation";
|
|
import { isAuthenticated, isLoading as isAuthLoading } from "$lib/stores/auth";
|
|
import { useScrollGuard } from "$lib/composables/useScrollGuard";
|
|
import { useScrollRestore } from "$lib/utils/scrollContainer";
|
|
import AppHeader from "$lib/components/AppHeader.svelte";
|
|
import BottomUi from "$lib/components/BottomUi.svelte";
|
|
import SleepTimerModal from "$lib/components/player/SleepTimerModal.svelte";
|
|
|
|
// Scroll guard prevents accidental taps on library cards during/after scrolling (Android)
|
|
const scrollGuard = useScrollGuard(300);
|
|
setContext("scrollGuard", scrollGuard);
|
|
|
|
// This scroller outlives every /library/* route rendered into it, so opening
|
|
// an item from half-way down a grid used to drop the viewer half-way down the
|
|
// detail page. Registered at init, as SvelteKit's nav hooks require. (DR-156)
|
|
let libraryScroller = $state<HTMLElement>();
|
|
useScrollRestore(() => libraryScroller, "library");
|
|
|
|
let { children } = $props();
|
|
|
|
let showSleepTimerModal = $state(false);
|
|
|
|
onMount(() => {
|
|
return () => {
|
|
scrollGuard.cleanup();
|
|
};
|
|
});
|
|
|
|
// Redirect to login if not authenticated
|
|
$effect(() => {
|
|
if (!$isAuthLoading && !$isAuthenticated) {
|
|
goto("/");
|
|
}
|
|
});
|
|
|
|
// Search lives in AppHeader (see HeaderSearch.svelte) so the same bar renders
|
|
// here and on /search — this layout no longer owns any search state.
|
|
</script>
|
|
|
|
{#if $isAuthLoading}
|
|
<div class="min-h-full flex items-center justify-center">
|
|
<div
|
|
class="w-8 h-8 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"
|
|
></div>
|
|
</div>
|
|
{:else if $isAuthenticated}
|
|
<div class="h-full flex flex-col overflow-hidden">
|
|
<!-- Header (shared across all authenticated chrome; it owns the search bar) -->
|
|
<AppHeader />
|
|
|
|
<!-- Main content. The BottomUi below is an in-flow flex sibling, so this
|
|
scroller is physically bounded above it and its last row can never
|
|
render behind the nav — no measurement, no reserved padding. -->
|
|
<main
|
|
bind:this={libraryScroller}
|
|
class="flex-1 overflow-y-auto p-4 min-h-0"
|
|
style="overscroll-behavior: contain"
|
|
onscroll={scrollGuard.onScroll}
|
|
>
|
|
{@render children()}
|
|
</main>
|
|
|
|
<!-- Bottom UI (mini player + nav), in-flow below the scroller. -->
|
|
<BottomUi />
|
|
|
|
<!-- Sleep Timer Modal -->
|
|
<SleepTimerModal isOpen={showSleepTimerModal} onClose={() => (showSleepTimerModal = false)} />
|
|
</div>
|
|
{/if}
|