forked from Keelrleek/SoulStream
Preview modal audio, five rounds: - True track-change crossfade: scheduleSettle no longer silences the outgoing clip at t=0; preview_load's equal-power fade owns both ramps, gated on the incoming voice's first Playing event (audible_marks), with failure/overstay fallbacks. CROSSFADE_MS = 340 — locked to the reel's card-slide so audio and visuals land together. - Two-neighbour lease prefetch + a 15-min on-disk clip byte-cache (recent_clips): steps in BOTH directions crossfade in ~15 ms; revisits never re-download. - Adaptive settle (isolated steps fire immediately, bursts coalesce) and anticipatory auto-advance (step at clip end minus 400 ms) so auto-play crossfades over the tail instead of gapping. - Session resume memory: revisited tracks continue where they left off (decoder-level start offset via play_local_from/resume_at); finished tracks restart from the top. - Open/close crossfades with the MAIN player: fade-pause with the desired-paused latch landing at ramp end, fade-resume from the paused position; MAIN_FADE_GEN guards; focus handshake relaxation documented in focusBridge. Volume normalization (Settings > Playback, default on): - Dependency-free BS.1770/EBU R128 meter in the player crate (K-weighting, 400 ms gated blocks, incremental) + analyzer that reads the same growing file playback streams from — first plays are measured at download speed. - Gain layered under user volume; audible start GATED on a preliminary measurement (10 s of audio or 800 ms budget) so the level is right from the first sample and never changes mid-track; prelim boosts capped 3 dB, peak-aware clipping guard, persistent loudness-cache.json. - Gapless boundaries get their own sample-accurate gain via SetNextNormGainDb, fed by preload analysis. Debug overlay (Ctrl+Shift+D): process-tree CPU / PDH GPU / working-set + private-commit RAM / network rates / FPS / current-track download progress read from the backend's own StreamProgress / live normalization state / preview session phase. Polls only while visible. 131 JS + player + app tests green; every path verified live over CDP.
136 lines
5.1 KiB
Svelte
136 lines
5.1 KiB
Svelte
<script>
|
|
import Sidebar from "./components/Sidebar.svelte";
|
|
import TitleBar from "./components/TitleBar.svelte";
|
|
import PlayerBar from "./components/PlayerBar.svelte";
|
|
import NowPlaying from "./components/NowPlaying.svelte";
|
|
import ArtworkViewer from "./components/ArtworkViewer.svelte";
|
|
import LoginModal from "./components/LoginModal.svelte";
|
|
import PreviewOverlay from "./components/PreviewOverlay.svelte";
|
|
import DebugOverlay from "./components/DebugOverlay.svelte";
|
|
import { previewUi } from "./lib/preview/previewSession.svelte.js";
|
|
import Home from "./views/Home.svelte";
|
|
import Search from "./views/Search.svelte";
|
|
import Library from "./views/Library.svelte";
|
|
import Downloads from "./views/Downloads.svelte";
|
|
import Settings from "./views/Settings.svelte";
|
|
import Playlist from "./views/Playlist.svelte";
|
|
import CatalogSearch from "./views/CatalogSearch.svelte";
|
|
import CatalogArtist from "./views/CatalogArtist.svelte";
|
|
import CatalogAlbum from "./views/CatalogAlbum.svelte";
|
|
import {
|
|
ui,
|
|
initEvents,
|
|
autoConnect,
|
|
refreshLibrary,
|
|
refreshPlaylists,
|
|
} from "./lib/state.svelte.js";
|
|
import { floatScroll } from "./lib/scrollbar.js";
|
|
|
|
const views = {
|
|
home: Home,
|
|
search: Search,
|
|
library: Library,
|
|
downloads: Downloads,
|
|
settings: Settings,
|
|
playlist: Playlist,
|
|
catalog: CatalogSearch,
|
|
catalogArtist: CatalogArtist,
|
|
catalogAlbum: CatalogAlbum,
|
|
};
|
|
const CurrentView = $derived(views[ui.view] ?? Home);
|
|
|
|
initEvents();
|
|
autoConnect();
|
|
refreshLibrary();
|
|
refreshPlaylists();
|
|
|
|
</script>
|
|
|
|
<div
|
|
class="app"
|
|
class:np-open={ui.nowPlaying}
|
|
class:sidebar-collapsed={ui.sidebarCollapsed}
|
|
class:preview-open={previewUi.open}
|
|
>
|
|
<TitleBar />
|
|
<!-- While the preview modal is open the page is inert and the flanking
|
|
panels + player bar are pointer-inert under the scrim (user decision
|
|
2026-07-23: the bar stays VISIBLE, dimmed, like Spotify — un-rendering
|
|
it left a black hole in the grid's bottom row). There is still no UI
|
|
path to command main audio behind the focus coordinator's back: the
|
|
scrim intercepts pointer input and the dialog traps the keyboard.
|
|
The titlebar stays live for window chrome. -->
|
|
<Sidebar />
|
|
<main class="content" use:floatScroll inert={previewUi.open || undefined}>
|
|
<CurrentView />
|
|
</main>
|
|
<NowPlaying />
|
|
<PlayerBar />
|
|
<PreviewOverlay />
|
|
<ArtworkViewer />
|
|
<LoginModal />
|
|
<DebugOverlay />
|
|
</div>
|
|
|
|
<style>
|
|
/* Frameless window: the titlebar row is part of the app background —
|
|
no separate chrome color, the panels float beneath it. */
|
|
/* While the preview modal is open: keyboard is contained by the dialog's
|
|
focus trap and aria-modal, the content pane is `inert`, and the flanking
|
|
panels + player bar stop taking pointer input here — the bar stays
|
|
visible (dimmed under the scrim) but cannot command main audio. */
|
|
.app.preview-open :global(.sidebar),
|
|
.app.preview-open :global(.nowplaying),
|
|
.app.preview-open :global(.player) {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.app {
|
|
display: grid;
|
|
grid-template-columns: var(--sidebar-width) 1fr var(--nowplaying-width);
|
|
grid-template-rows: 30px 1fr var(--player-height);
|
|
grid-template-areas:
|
|
"titlebar titlebar titlebar"
|
|
"sidebar content nowplaying"
|
|
"player player player";
|
|
height: 100%;
|
|
gap: 8px;
|
|
padding: 0 8px 8px;
|
|
background: var(--bg-sidebar);
|
|
overflow: hidden;
|
|
transition: grid-template-columns 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
/* Collapsed left nav: narrow the sidebar column to an icon rail. The
|
|
grid-template-columns transition animates the reflow. */
|
|
.app.sidebar-collapsed {
|
|
--sidebar-width: 64px;
|
|
}
|
|
|
|
/* Now Playing panel slides away: the column collapses to zero (the panel
|
|
itself keeps its width and slides off-screen — see NowPlaying.svelte). */
|
|
.app:not(.np-open) {
|
|
--nowplaying-width: 0px;
|
|
}
|
|
|
|
.content {
|
|
grid-area: content;
|
|
background: linear-gradient(var(--bg-highlight) 0, var(--bg-base) 240px);
|
|
/* The highlight wash belongs to the top of the PAGE, not the viewport:
|
|
anchored to the pane (default `scroll`), the gradient stays put
|
|
while content scrolls, so the hero's scrim — which dissolves into
|
|
pure bg-base — ends on a lighter backdrop mid-gradient and paints a
|
|
hard seam at the hero's bottom edge. `local` scrolls the gradient
|
|
away with the content, keeping the seam colors identical at every
|
|
scroll position. */
|
|
background-attachment: local;
|
|
border-radius: var(--radius);
|
|
overflow-y: auto;
|
|
overflow-anchor: none;
|
|
/* Main scroll pane: no elastic rubber-band at its edges. Inner regions
|
|
deliberately do NOT set this, so their wheel chains here. */
|
|
overscroll-behavior: none;
|
|
padding: 24px 28px;
|
|
}
|
|
</style>
|