From 10b3bbbf2ed1e99b03e0aa3adeb786c90d0a57ba Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Mon, 8 Jun 2026 16:53:17 -0300 Subject: [PATCH] feat: multi-player support + 5-line karaoke lyrics display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get-playing.sh: iterates playerctl players, picks the one actually Playing; QML now calls this instead of bare playerctl metadata so a paused Spotify no longer blocks a playing YouTube from showing - dancer.html: replace single lyric line with 5-slot karaoke display — active line (center) is full-brightness with theme-color glow; 2 lines before/after are progressively dimmed; slots fade out/in (0.18s/0.45s) when the active line advances Co-Authored-By: Claude Sonnet 4.6 --- plugin/contents/ui/dancer.html | 113 +++++++++++++++++++++++++----- plugin/contents/ui/get-playing.sh | 8 +++ plugin/contents/ui/main.qml | 2 +- 3 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 plugin/contents/ui/get-playing.sh diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html index 75b0ab3..1e69110 100644 --- a/plugin/contents/ui/dancer.html +++ b/plugin/contents/ui/dancer.html @@ -237,23 +237,46 @@ html, body { } /* ============================================================ - LYRICS DISPLAY + LYRICS DISPLAY — 5-line karaoke style ============================================================ */ .lyric-display { position: absolute; left: 2%; top: 4%; - max-width: 36%; + max-width: 38%; pointer-events: none; opacity: 0; transition: opacity 0.5s ease; } -.lyric-line { - color: #ffffff; +.lyric-ctx { font-family: 'Courier New', monospace; - font-size: clamp(16px, 1.3vw, 18px); line-height: 1.5; - letter-spacing: 0.03em; + letter-spacing: 0.02em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: opacity 0.45s ease; +} +/* slot 1: far past */ +.lyric-ctx:nth-child(1) { + color: #888; + font-size: clamp(11px, 0.85vw, 13px); + opacity: 0.22; + margin-bottom: 1px; +} +/* slot 2: near past */ +.lyric-ctx:nth-child(2) { + color: #bbb; + font-size: clamp(13px, 1.0vw, 15px); + opacity: 0.48; + margin-bottom: 3px; +} +/* slot 3: active */ +.lyric-ctx:nth-child(3) { + color: #fff; + font-size: clamp(16px, 1.3vw, 18px); + opacity: 1; + margin-bottom: 3px; text-shadow: 0 0 18px var(--theme-color, rgba(0,200,255,0.85)), 0 0 6px var(--theme-color, rgba(0,200,255,0.5)), @@ -261,6 +284,19 @@ html, body { -1px -1px 0 rgba(0,0,0,0.7), 1px 1px 0 rgba(0,0,0,0.7); } +/* slot 4: near future */ +.lyric-ctx:nth-child(4) { + color: #bbb; + font-size: clamp(13px, 1.0vw, 15px); + opacity: 0.48; + margin-bottom: 1px; +} +/* slot 5: far future */ +.lyric-ctx:nth-child(5) { + color: #888; + font-size: clamp(11px, 0.85vw, 13px); + opacity: 0.22; +} /* ============================================================ SCANLINES OVERLAY @@ -887,9 +923,13 @@ html, body { - +
-
+
+
+
+
+
@@ -1166,10 +1206,48 @@ function applyTheme(rgb) { const albumArtEl = document.getElementById('albumArt'); const fogWisps = document.querySelectorAll('.fog-wisp'); const lyricDisplay = document.getElementById('lyricDisplay'); -const lyricLineEl = document.getElementById('lyricLine'); +const lyricCtxEls = Array.from(document.querySelectorAll('.lyric-ctx')); -let lastArtUrl = ''; -let lastLyricLine = ''; +let lastArtUrl = ''; +let currentLyricKey = ''; +let lyricFadeTimer = null; + +function updateLyricContext(context) { + if (!context || !context.length || !isPlaying) { + lyricDisplay.style.opacity = '0'; + return; + } + const active = context.find(c => c.offset === 0); + if (!active || !active.text) { + lyricDisplay.style.opacity = '0'; + return; + } + const newKey = context.map(c => c.text).join('\x00'); + if (newKey === currentLyricKey) { + lyricDisplay.style.opacity = '1'; + return; + } + currentLyricKey = newKey; + + const wasVisible = lyricDisplay.style.opacity === '1'; + if (wasVisible) { + if (lyricFadeTimer) clearTimeout(lyricFadeTimer); + lyricCtxEls.forEach(el => { + el.style.transition = 'opacity 0.18s ease'; + el.style.opacity = '0'; + }); + lyricFadeTimer = setTimeout(() => { + lyricFadeTimer = null; + context.forEach((item, i) => { lyricCtxEls[i].textContent = item.text; }); + lyricCtxEls.forEach(el => { el.style.transition = ''; el.style.opacity = ''; }); + lyricDisplay.style.opacity = '1'; + }, 200); + } else { + context.forEach((item, i) => { lyricCtxEls[i].textContent = item.text; }); + lyricCtxEls.forEach(el => { el.style.opacity = ''; }); + lyricDisplay.style.opacity = '1'; + } +} function fetchCurrent() { fetch('http://127.0.0.1:8765/current') @@ -1181,12 +1259,7 @@ function fetchCurrent() { document.getElementById('albumArtImg').src = 'http://127.0.0.1:8765/art?' + Date.now(); } - const line = d.lyric_line || ''; - if (line !== lastLyricLine) { - lastLyricLine = line; - lyricLineEl.textContent = line; - } - lyricDisplay.style.opacity = (isPlaying && line) ? '1' : '0'; + updateLyricContext(d.lyric_context || []); }) .catch(() => {}); } @@ -1246,7 +1319,11 @@ function updateMusic(artist, title) { document.querySelector('.robot-wrap').style.opacity = playing ? '1' : '0'; document.querySelector('.music-ticker').style.opacity = playing ? '1' : '0'; albumArtEl.style.opacity = playing ? '1' : '0'; - if (!playing) vuBars.forEach(b => { b.style.height = '3px'; b.style.opacity = '0.15'; }); + if (!playing) { + vuBars.forEach(b => { b.style.height = '3px'; b.style.opacity = '0.15'; }); + lyricDisplay.style.opacity = '0'; + currentLyricKey = ''; + } if (playing) fetchCurrent(); const key = artist + '|' + title; if (el.dataset.key === key) return; diff --git a/plugin/contents/ui/get-playing.sh b/plugin/contents/ui/get-playing.sh new file mode 100644 index 0000000..47a942e --- /dev/null +++ b/plugin/contents/ui/get-playing.sh @@ -0,0 +1,8 @@ +#!/bin/bash +for player in $(playerctl -l 2>/dev/null); do + if [ "$(playerctl --player "$player" status 2>/dev/null)" = "Playing" ]; then + playerctl --player "$player" metadata --format "{{status}}||{{artist}}||{{title}}" 2>/dev/null + exit 0 + fi +done +printf "||" diff --git a/plugin/contents/ui/main.qml b/plugin/contents/ui/main.qml index 173dcae..7402871 100644 --- a/plugin/contents/ui/main.qml +++ b/plugin/contents/ui/main.qml @@ -38,7 +38,7 @@ WallpaperItem { Plasma5Support.DataSource { id: musicSource engine: "executable" - connectedSources: ["playerctl metadata --format '{{status}}||{{artist}}||{{title}}' 2>/dev/null || echo '||'"] + connectedSources: ["bash " + Qt.resolvedUrl("get-playing.sh").toString().replace("file://", "")] interval: 2000 onNewData: function(sourceName, data) {