layout improvements
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 9m2s
Traceability Validation / Check Requirement Traces (push) Successful in 2m30s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been cancelled

This commit is contained in:
2026-06-28 20:14:17 +02:00
parent ef7be645b3
commit 8eae4ae253
12 changed files with 403 additions and 59 deletions
+72 -33
View File
@@ -17,13 +17,49 @@
import MiniPlayer from "$lib/components/player/MiniPlayer.svelte";
import SleepTimerModal from "$lib/components/player/SleepTimerModal.svelte";
import BottomNav from "$lib/components/BottomNav.svelte";
import { isInitialized, pendingSyncCount, isAndroid, showSleepTimerModal } from "$lib/stores/appState";
import { isInitialized, pendingSyncCount, isAndroid, showSleepTimerModal, bottomUiHeight } from "$lib/stores/appState";
// Shuffle/repeat/next/previous come from the event-driven queue store, the
// single source of truth (updated instantly on queue_changed).
import { isShuffle as shuffle, repeatMode as repeat, hasNext, hasPrevious } from "$lib/stores/queue";
let { children } = $props();
// The fixed bottom UI (mini player stacked over the bottom nav) is measured in
// real time and its height published to `bottomUiHeight`, so pages can reserve
// exactly that much space instead of guessing fixed rem values.
let bottomUiEl = $state<HTMLElement | null>(null);
// Route-level visibility for the fixed bottom UI (the mini player itself also
// self-gates on playback state; when it renders nothing the in-flow slot
// collapses to 0 and the ResizeObserver shrinks the reserved padding).
const pathname = $derived($page.url.pathname);
const showBottomNav = $derived(
$isAuthenticated && !pathname.startsWith('/player/') && !pathname.startsWith('/login')
);
const showGlobalMiniPlayer = $derived(
!pathname.startsWith('/player/') &&
!pathname.startsWith('/login') &&
!pathname.startsWith('/settings') &&
($isAndroid || !pathname.startsWith('/library'))
);
$effect(() => {
const el = bottomUiEl;
if (!el) {
bottomUiHeight.set(0);
return;
}
const ro = new ResizeObserver((entries) => {
bottomUiHeight.set(entries[0]?.contentRect.height ?? el.offsetHeight);
});
ro.observe(el);
bottomUiHeight.set(el.offsetHeight);
return () => {
ro.disconnect();
bottomUiHeight.set(0);
};
});
onMount(async () => {
// Detect platform first (synchronously, before any await) so the global
// mini player's Android visibility gate is correct from the first render.
@@ -131,40 +167,43 @@
<!-- Toast notifications (global) -->
<Toast />
<!-- Bottom Navigation (mobile only - show everywhere except player and login) -->
{#if $isAuthenticated && !$page.url.pathname.startsWith('/player/') && !$page.url.pathname.startsWith('/login')}
<BottomNav />
{/if}
<!-- Fixed bottom UI: mini player stacked over the bottom nav, both in normal
flow inside one measured wrapper. The wrapper's height is observed and
published to `bottomUiHeight` so pages reserve exactly this much space.
Mini player is first (visually on top, above the nav). -->
{#if showBottomNav || showGlobalMiniPlayer}
<div bind:this={bottomUiEl} class="fixed bottom-0 left-0 right-0 z-40 flex flex-col">
{#if showGlobalMiniPlayer}
<MiniPlayer
media={$currentMedia}
isPlaying={$isPlaying}
position={$playbackPosition}
duration={$playbackDuration}
shuffle={$shuffle}
repeat={$repeat}
hasNext={$hasNext}
hasPrevious={$hasPrevious}
className="flex-shrink-0"
onExpand={() => {
// Navigate to player page when mini player is expanded
if ($currentMedia) {
goto(`/player/${$currentMedia.id}`);
}
}}
onSleepTimerClick={() => showSleepTimerModal.set(true)}
/>
{/if}
<!-- Mini Player - show everywhere except on full player page, login and settings -->
<!-- Android: Show on all routes (except player/login/settings) -->
<!-- Desktop: Show on non-library routes (library layout has its own MiniPlayer) -->
{#if !$page.url.pathname.startsWith('/player/') && !$page.url.pathname.startsWith('/login') && !$page.url.pathname.startsWith('/settings')}
{#if $isAndroid || !$page.url.pathname.startsWith('/library')}
<MiniPlayer
media={$currentMedia}
isPlaying={$isPlaying}
position={$playbackPosition}
duration={$playbackDuration}
shuffle={$shuffle}
repeat={$repeat}
hasNext={$hasNext}
hasPrevious={$hasPrevious}
onExpand={() => {
// Navigate to player page when mini player is expanded
if ($currentMedia) {
goto(`/player/${$currentMedia.id}`);
}
}}
onSleepTimerClick={() => showSleepTimerModal.set(true)}
/>
{#if showBottomNav}
<BottomNav className="flex-shrink-0" />
{/if}
</div>
<!-- Sleep Timer Modal -->
<SleepTimerModal
isOpen={$showSleepTimerModal}
onClose={() => showSleepTimerModal.set(false)}
/>
{/if}
<!-- Sleep Timer Modal -->
<SleepTimerModal
isOpen={$showSleepTimerModal}
onClose={() => showSleepTimerModal.set(false)}
/>
{/if}
{:else}
<div class="flex items-center justify-center h-screen">