// LyricsFocusPanel — FOCUS mode 4, the giant-line lyrics variant (§6.6 of // the 2026-08-02 immersive-port contract), port of // crates/qbz-ui/ui/immersive/ImmersiveLyricsFocusPanel.slint:50-151. // // One giant centered CURRENT line, cross-faded on every line change; NO // karaoke fill (whole-line swap only, exactly like the Slint/Svelte source // which omits activeProgress). Data: the QbzLyrics doc (lines/status/synced, // passed in by the mount) + the SHARED immersive LyricsSyncEngine instance // from ImmersiveView (`sync`) — the Slint reads the one global LyricsState // active-index; Qt has one engine per surface group, and the focus panel + // the split lyrics share the immersive engine (trap 22: its gateOpen covers // BOTH lyric surfaces and nothing else). // // Entry animation (:57-74,:116-125): fadeInUp-LITE (translate-up 24px + // opacity; the blur is skipped per the recon Slint note) replayed WITHOUT // remounting: a watch on sync.activeIndex flips `entering` (SNAP to the // start pose — the Behaviors are DISABLED while entering so the pose jump // does not animate), a 16ms one-shot releases it and the 500ms // cubic-bezier(0.22,1,0.36,1) runs forward. // // The line takes QbzShell.ambientAccent — the album-art accent that is the // COMPLEMENT of the cover's dominant hue (ImmersiveState.lyrics-accent), // NOT the UI lyrics color, NOT plain white (:102-107). import QtQuick import com.blitzfc.qbz Item { id: root // ---- data (supplied by the ImmersiveView mount) ---------------------- // The QbzLyrics document fields the panel reads. property var lines: [] property bool synced: false property int status: 0 // The SHARED immersive LyricsSyncEngine instance (see the header). property var sync: null readonly property int activeIndex: root.sync ? root.sync.activeIndex : -1 // Current line text (empty if no active line yet, :34-36). readonly property string currentLine: (root.activeIndex >= 0 && root.activeIndex < root.lines.length && root.lines[root.activeIndex].text !== undefined) ? root.lines[root.activeIndex].text : "" readonly property bool hasCurrent: root.currentLine !== "" // Synced docs with no current line yet (before the first lyric) show ♪ // (:40-42). readonly property bool waiting: root.synced && root.status === 2 && root.lines.length > 0 && !root.hasCurrent // Truly empty: ready/no-lyrics with zero lines, or idle (:44-46). readonly property bool noLyrics: root.status === 3 || (root.status === 2 && root.lines.length === 0) // Responsive font size (clamp(28px, 5vw, 56px), :50-51) — vw = the // panel's own width (full-viewport in the focus placement). readonly property real lineSize: Math.max(28, Math.min(root.width * 0.05, 56)) // --- Entry-animation retrigger (:57-74) -------------------------------- readonly property int watchedIndex: root.activeIndex property bool entering: false onWatchedIndexChanged: { // Snap to the start pose, then release on the next tick so the // Behaviors run forward (toggling within one evaluation would not // produce a transition — the Behaviors are disabled while entering). root.entering = true enterTimer.restart() } Timer { id: enterTimer interval: 16 repeat: false onTriggered: root.entering = false } // The line block, centered as a group in the viewport; padding mirrors // the Svelte 80px 60px 140px (top / sides / bottom-player-clearance, // :83-86). Item { anchors.fill: parent anchors.topMargin: 80 anchors.leftMargin: 60 anchors.rightMargin: 60 anchors.bottomMargin: 140 // Current line — fadeInUp-lite (:100-126). Centered in the block. Text { visible: root.hasCurrent anchors.centerIn: parent // width min(parent, vw*0.9) (:114). width: Math.min(parent.width, root.width * 0.9) text: root.currentLine color: QbzShell.ambientAccent font.pixelSize: root.lineSize font.weight: Font.Bold // 700 font.letterSpacing: 0.2 horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap // The translate-up is applied via anchors.verticalCenterOffset so // the centering anchor survives the offset. anchors.verticalCenterOffset: root.entering ? 24 : 0 opacity: root.entering ? 0.0 : 1.0 Behavior on anchors.verticalCenterOffset { // Disabled while entering -> the start pose SNAPS; the // release (entering -> false) animates forward (:118-121). enabled: !root.entering NumberAnimation { duration: 500 easing.type: Easing.Bezier easing.bezierCurve: [0.22, 1.0, 0.36, 1.0] } } Behavior on opacity { enabled: !root.entering NumberAnimation { duration: 500 easing.type: Easing.Bezier easing.bezierCurve: [0.22, 1.0, 0.36, 1.0] } } } // Synced-but-before-first-line: the ♪ glyph (:130-138). Text { visible: !root.hasCurrent && root.waiting anchors.centerIn: parent text: "♪" color: "#ffffff" opacity: 0.3 font.pixelSize: 48 } // No lyrics at all — localized empty state (:142-150). Slint // #ffffff80 (RRGGBBAA) -> Qt #AARRGGBB. Text { visible: !root.hasCurrent && !root.waiting && root.noLyrics anchors.centerIn: parent width: parent.width text: QbzSession.tr("No lyrics", QbzSession.trRev) color: "#80ffffff" font.pixelSize: 20 font.italic: true horizontalAlignment: Text.AlignHCenter } } }