feat(slint): lyrics wiring + static sidebar rendering
S3 of the Lyrics epic: the qbz-lyrics engine reaches the Slint shell — per-user cache binding, the track-edge fetch rider, the LyricsState global, and the real sidebar body replacing the placeholder. - Glue (src/lyrics.rs, offline_mode/fav_cache template): process-global LyricsService over the core client lock (read at call time, never cached); init_for_user on ALL session activation paths (browser login, saved-session restore, offline entry) binding the SAME per-user lyrics/lyrics.db Tauri uses; teardown on logout. - Fetch rider on the NOTIFY_LAST_TRACK dedupe edge in refresh_now_playing_meta, next to scrobble/notify — prefetch on every track change regardless of panel visibility (Tauri parity, lyricsStore.ts:545-565) and deliberately NO skip-if-remote (Q7: lyrics follow the QConnect peer). Track->null resets the state. Request carries source kind (qobuz_download counts as Qobuz, real catalog id) + the offline engine verdict; raw title to the engine (F4). - Stale guard (F2): responses echo the request identity and are dropped unless they still match the latest requested track. Duplicate-fetch guard mirrors Tauri's lastFetchedTrackId (skip only while loaded). Parsed doc (native wsync words) held Rust-side for the S4 engine. - LyricsState global: line model (text, time-ms, end-ms, has-words), status (idle/loading/ready/no-lyrics/error/offline), synced, provider + attribution label, header meta, active-index placeholder (-1), and panel-opened (fetch-on-open via conditional-mount init). - LyricsLinesView (shared, miniplayer-ready): 280px-adapted typography per the owner Q3 spec - 13/15px reduced scale, 0.2px letter-spacing, 6px side padding, full-width wrapped text; dimming ladder on the INNER Text (never a root opacity binding); centering spacers from the Flickable's own height; plain mode = static top-aligned at 0.85; fallback-provider attribution line (Qobuz first-party = none). - Sidebar 1:1 header (mic tile 36px, LYRICS overline 11px/1.5px per the alignment standard, title-artist meta, sliders trigger for the S5 flyout, close kept) + state bodies incl. the typed NotAvailableOffline as a translated string (F3). All new strings @tr()-wrapped. Sync engine (position, active line, auto-center, karaoke fill) is S4. cc
This commit is contained in:
11 files changed
+793
-20
No files matched your search
@@ -1,16 +1,25 @@
|
||||
// Right-side Lyrics panel.
|
||||
//
|
||||
// Rendered as a layout sibling of the content area (see AppShell), so it
|
||||
// shrinks the content rather than overlaying it. Closing sets
|
||||
// ShellState.right-panel = 0. The body is a structural placeholder;
|
||||
// lyric contents are wired in a later milestone.
|
||||
// shrinks the content rather than overlaying it. The 280px column width is
|
||||
// owned by AppShell (owner Q3: stays 280px; the typography adapts in
|
||||
// LyricsLinesView instead). Closing sets ShellState.lyrics-open = false.
|
||||
//
|
||||
// 1:1 to the Tauri `LyricsSidebar.svelte` header/states (spec §3.1): 36px
|
||||
// mic icon tile + LYRICS overline + "{title} - {artist}" meta + the
|
||||
// display-settings trigger (S5 wires the LyricsControlsFlyout), plus the
|
||||
// Slint-convention close button (deviation D4, homologated with Queue).
|
||||
// Body renders by LyricsState.status: loading / no-lyrics / offline-miss
|
||||
// (typed NotAvailableOffline, F3) / error / the shared lines view.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { ShellState } from "../state.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { LyricsLinesView } from "LyricsLinesView.slint";
|
||||
import { ShellState, LyricsState } from "../state.slint";
|
||||
|
||||
component CloseButton inherits Rectangle {
|
||||
callback clicked();
|
||||
@@ -36,30 +45,109 @@ component CloseButton inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// 32px icon button — the display-settings trigger (Tauri SlidersHorizontal,
|
||||
// LyricsSidebar.svelte:38-66).
|
||||
component HeaderIconButton inherits Rectangle {
|
||||
in property <image> icon;
|
||||
callback clicked();
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: Radius.sm;
|
||||
background: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component LyricsSidebar inherits Rectangle {
|
||||
// Width is owned by the right-panel column in AppShell; this fills it and
|
||||
// vertical-stretches to its share of the height (stacked under the queue).
|
||||
clip: true;
|
||||
background: Theme.surface-card;
|
||||
|
||||
// Conditionally mounted (`if lyrics-open` at the AppShell mount site,
|
||||
// ADR-010) — re-request lyrics for the current track on every open.
|
||||
// Cache-served / no-op while already loaded (Tauri parity: load lands
|
||||
// immediately when opening mid-song, lyricsStore.ts:386-389).
|
||||
init => {
|
||||
LyricsState.panel-opened();
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 0px;
|
||||
|
||||
// Header row — title + close button.
|
||||
// Header: icon tile + overline/meta block + controls trigger + close.
|
||||
// Taller than the Queue header (56px vs 44px) to fit the Tauri
|
||||
// two-row title block + 36px tile (spec §3.1 rows 3-5).
|
||||
HorizontalLayout {
|
||||
padding-left: Spacing.md;
|
||||
padding-left: Spacing.sm;
|
||||
padding-right: Spacing.sm;
|
||||
padding-top: Spacing.sm;
|
||||
padding-bottom: Spacing.sm;
|
||||
height: 44px;
|
||||
Text {
|
||||
text: @tr("Lyrics");
|
||||
color: Theme.text-primary;
|
||||
// Homologated with the Queue / History tab size.
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
vertical-alignment: center;
|
||||
height: 56px;
|
||||
spacing: Spacing.sm;
|
||||
|
||||
// Icon tile — 36px, mic-vocal 18, accent tint.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: Radius.sm;
|
||||
background: Theme.surface-elevated;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/mic-vocal.svg");
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: Theme.accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Title block: overline + playing-track meta.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 2px;
|
||||
horizontal-stretch: 1;
|
||||
Text {
|
||||
// GroupHeader overline per the alignment standard (ALL-CAPS
|
||||
// 11px / 1.5px letter-spacing — deviation D8 vs Tauri's
|
||||
// 12px/0.1em, forced by the binding standard).
|
||||
text: @tr("LYRICS");
|
||||
color: Theme.text-muted;
|
||||
font-size: 11px;
|
||||
font-weight: Typography.semibold;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
if LyricsState.track-title != "": Text {
|
||||
text: LyricsState.track-title + " - " + LyricsState.track-artist;
|
||||
color: Theme.text-secondary;
|
||||
font-size: 12px;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
// Display-settings trigger — the LyricsControlsFlyout lands in S5.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
HeaderIconButton {
|
||||
icon: @image-url("../assets/icons/sliders-horizontal.svg");
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
@@ -75,13 +163,77 @@ export component LyricsSidebar inherits Rectangle {
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
|
||||
// Body — placeholder until lyric contents are wired.
|
||||
// Body — by LyricsState.status.
|
||||
Rectangle {
|
||||
vertical-stretch: 1;
|
||||
Text {
|
||||
text: @tr("No lyrics");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
clip: true;
|
||||
|
||||
// Loading: 24px spinner + label (Tauri LyricsSidebar.svelte:68-92).
|
||||
if LyricsState.status == 1: VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 10px;
|
||||
padding: Spacing.md;
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
LoadingSpinner {
|
||||
size: 24px;
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: @tr("Fetching lyrics...");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
// No lyrics — idle, not-found, or a ready result that parsed to
|
||||
// zero lines.
|
||||
if LyricsState.status == 0
|
||||
|| LyricsState.status == 3
|
||||
|| (LyricsState.status == 2 && LyricsState.lines.length == 0): VerticalLayout {
|
||||
alignment: center;
|
||||
padding: Spacing.md;
|
||||
Text {
|
||||
text: @tr("No lyrics available");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
horizontal-alignment: center;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Offline with nothing cached — the typed NotAvailableOffline
|
||||
// outcome (F3: a translated string, never hardcoded EN).
|
||||
if LyricsState.status == 5: VerticalLayout {
|
||||
alignment: center;
|
||||
padding: Spacing.md;
|
||||
Text {
|
||||
text: @tr("Lyrics unavailable offline");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
horizontal-alignment: center;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Error — surfaced backend text, standard error tone.
|
||||
if LyricsState.status == 4: VerticalLayout {
|
||||
alignment: center;
|
||||
padding: Spacing.md;
|
||||
Text {
|
||||
text: LyricsState.error;
|
||||
color: #e0564f;
|
||||
font-size: Typography.legal;
|
||||
horizontal-alignment: center;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Lyrics body — the shared lines renderer.
|
||||
if LyricsState.status == 2 && LyricsState.lines.length > 0: LyricsLinesView {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user