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
145 lines
6.1 KiB
Plaintext
145 lines
6.1 KiB
Plaintext
// Shared lyrics lines renderer — consumed by the sidebar now, the miniplayer
|
|
// surface later (spec §3.6: everything arrives via LyricsState + properties;
|
|
// nothing here references the sidebar, ShellState, or window geometry).
|
|
//
|
|
// 1:1 to Tauri's `LyricsLines.svelte` compact mode, ADAPTED to the OWNER's
|
|
// 280px typography spec (Q3): reduced sizes vs the 340px Tauri column
|
|
// (13/15px = Tauri's compact "small" tier instead of the 15/17 medium),
|
|
// friendly 0.2px letter-spacing, minimal 6px side padding (never removed),
|
|
// the text takes the full remaining width.
|
|
//
|
|
// Static rendering only (S3): the S4 sync engine drives
|
|
// `LyricsState.active-index` and adds auto-center scroll + the karaoke fill;
|
|
// until then scrolling is a plain Flickable and active-index stays -1.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { LyricsState } from "../state.slint";
|
|
|
|
// One lyric line. FOOTGUN (memory `reference_slint_root_opacity_absolute_position`):
|
|
// the dimming ladder is applied to the INNER Text (opacity + color), NEVER as
|
|
// an opacity binding on a row root — a root-opacity binding wraps the row in
|
|
// an opacity item that corrupts absolute-position / self.y anchoring inside
|
|
// the Flickable (one-row-pitch drift), exactly the geometry the S4 scroll
|
|
// math will rely on.
|
|
component LyricsLineRow inherits VerticalLayout {
|
|
in property <int> index;
|
|
in property <string> line-text;
|
|
in property <length> size-inactive;
|
|
in property <length> size-active;
|
|
// 0 = off, 1 = soft, 2 = strong (Tauri default) — S5 wires the pref.
|
|
in property <int> dimming-mode;
|
|
|
|
property <bool> is-active:
|
|
LyricsState.synced && root.index == LyricsState.active-index;
|
|
property <bool> is-past:
|
|
LyricsState.synced
|
|
&& LyricsState.active-index >= 0
|
|
&& root.index < LyricsState.active-index;
|
|
property <int> distance: Math.abs(root.index - LyricsState.active-index);
|
|
|
|
// Dimming ladder (Tauri getLineOpacity, LyricsLines.svelte:342-356):
|
|
// off -> 1.0 all; soft -> 0.6 inactive; strong -> distance 1: 0.5,
|
|
// 2: 0.35, 3: 0.25, far: 0.15. Unsynced lyrics render the whole list at
|
|
// 0.85 (LyricsLines.svelte:515-523). With NO active line (-1) the ladder
|
|
// has no reference point — render full brightness (the S4 engine drives
|
|
// the index whenever a synced track plays).
|
|
pure function line-opacity() -> float {
|
|
if (!LyricsState.synced) {
|
|
return 0.85;
|
|
}
|
|
if (root.is-active) {
|
|
return 1.0;
|
|
}
|
|
if (root.dimming-mode == 0 || LyricsState.active-index < 0) {
|
|
return 1.0;
|
|
}
|
|
if (root.dimming-mode == 1) {
|
|
return 0.6;
|
|
}
|
|
if (root.distance == 1) {
|
|
return 0.5;
|
|
}
|
|
if (root.distance == 2) {
|
|
return 0.35;
|
|
}
|
|
if (root.distance == 3) {
|
|
return 0.25;
|
|
}
|
|
return 0.15;
|
|
}
|
|
|
|
Text {
|
|
text: root.line-text;
|
|
wrap: word-wrap;
|
|
opacity: root.line-opacity();
|
|
color: root.is-active
|
|
? Theme.text-primary
|
|
: (root.is-past ? Theme.text-muted : Theme.text-secondary);
|
|
font-size: root.is-active ? root.size-active : root.size-inactive;
|
|
font-weight: root.is-active ? Typography.bold : Typography.medium;
|
|
// Friendly letter-spacing (Q3): slightly open, never cramped.
|
|
letter-spacing: 0.2px;
|
|
}
|
|
}
|
|
|
|
export component LyricsLinesView inherits Rectangle {
|
|
// Owner Q3 layout knobs — 4-6px per side, NEVER removed; the miniplayer
|
|
// surface later overrides padding/gap without touching display prefs.
|
|
in property <length> side-padding: 6px;
|
|
in property <length> line-gap: 12px; // Tauri compact gap parity
|
|
// 280px-adapted type scale (Q3).
|
|
in property <length> size-inactive: 13px;
|
|
in property <length> size-active: 15px;
|
|
in property <int> dimming-mode: 2; // strong (Tauri default)
|
|
in property <bool> show-attribution: true;
|
|
|
|
// Top/bottom spacers so the first/last SYNCED lines can center once the
|
|
// S4 auto-scroll lands (Tauri's 40vh spacers, LyricsLines.svelte:510-513).
|
|
// Fixed-height Rectangles derived from the Flickable's OWN height — Slint
|
|
// layouts ignore percentage tricks (landing review §7.4), and sibling
|
|
// geometry would be a binding loop.
|
|
property <length> spacer-height:
|
|
Math.max(0px, flick.height / 2 - root.size-active * 1.5);
|
|
|
|
flick := Flickable {
|
|
width: 100%;
|
|
height: 100%;
|
|
viewport-width: self.width;
|
|
viewport-height: body.preferred-height;
|
|
|
|
body := VerticalLayout {
|
|
padding-left: root.side-padding;
|
|
padding-right: root.side-padding;
|
|
// Unsynced/plain mode is a static top-aligned list (no spacers,
|
|
// no active line, no scroll-follow — LyricsLines.svelte:515-523).
|
|
padding-top: LyricsState.synced ? 0px : 10px;
|
|
padding-bottom: LyricsState.synced ? 0px : 10px;
|
|
spacing: root.line-gap;
|
|
|
|
if LyricsState.synced: Rectangle { height: root.spacer-height; }
|
|
|
|
for line[idx] in LyricsState.lines: LyricsLineRow {
|
|
index: idx;
|
|
line-text: line.text;
|
|
size-inactive: root.size-inactive;
|
|
size-active: root.size-active;
|
|
dimming-mode: root.dimming-mode;
|
|
}
|
|
|
|
if LyricsState.synced: Rectangle { height: root.spacer-height; }
|
|
|
|
// Fallback-provider attribution (LRCLIB / lyrics.ovh). Tauri keeps
|
|
// these in the About modal (AboutModal.svelte:141,144); Slint has
|
|
// no About surface yet, so the line lives at the end of the scroll.
|
|
// Qobuz is first-party: provider-label is empty, nothing renders.
|
|
if root.show-attribution && LyricsState.provider-label != "": Text {
|
|
text: @tr("Lyrics provided by {}", LyricsState.provider-label);
|
|
color: Theme.text-muted;
|
|
font-size: 10px;
|
|
horizontal-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
}
|