feat: multi-player support + 5-line karaoke lyrics display

- 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 <noreply@anthropic.com>
This commit is contained in:
ltadeu6 2026-06-08 16:53:17 -03:00
parent 1a0f8c7ea5
commit 10b3bbbf2e
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D
3 changed files with 104 additions and 19 deletions

View file

@ -237,23 +237,46 @@ html, body {
} }
/* ============================================================ /* ============================================================
LYRICS DISPLAY LYRICS DISPLAY — 5-line karaoke style
============================================================ */ ============================================================ */
.lyric-display { .lyric-display {
position: absolute; position: absolute;
left: 2%; left: 2%;
top: 4%; top: 4%;
max-width: 36%; max-width: 38%;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
transition: opacity 0.5s ease; transition: opacity 0.5s ease;
} }
.lyric-line { .lyric-ctx {
color: #ffffff;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: clamp(16px, 1.3vw, 18px);
line-height: 1.5; 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: text-shadow:
0 0 18px var(--theme-color, rgba(0,200,255,0.85)), 0 0 18px var(--theme-color, rgba(0,200,255,0.85)),
0 0 6px var(--theme-color, rgba(0,200,255,0.5)), 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),
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 SCANLINES OVERLAY
@ -887,9 +923,13 @@ html, body {
</div> </div>
</div> </div>
<!-- Lyrics display --> <!-- Lyrics display — 5-line karaoke context -->
<div class="lyric-display" id="lyricDisplay"> <div class="lyric-display" id="lyricDisplay">
<div class="lyric-line" id="lyricLine"></div> <div class="lyric-ctx" id="lctx0"></div>
<div class="lyric-ctx" id="lctx1"></div>
<div class="lyric-ctx" id="lctx2"></div>
<div class="lyric-ctx" id="lctx3"></div>
<div class="lyric-ctx" id="lctx4"></div>
</div> </div>
<!-- Scanlines overlay --> <!-- Scanlines overlay -->
@ -1166,10 +1206,48 @@ function applyTheme(rgb) {
const albumArtEl = document.getElementById('albumArt'); const albumArtEl = document.getElementById('albumArt');
const fogWisps = document.querySelectorAll('.fog-wisp'); const fogWisps = document.querySelectorAll('.fog-wisp');
const lyricDisplay = document.getElementById('lyricDisplay'); const lyricDisplay = document.getElementById('lyricDisplay');
const lyricLineEl = document.getElementById('lyricLine'); const lyricCtxEls = Array.from(document.querySelectorAll('.lyric-ctx'));
let lastArtUrl = ''; let lastArtUrl = '';
let lastLyricLine = ''; 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() { function fetchCurrent() {
fetch('http://127.0.0.1:8765/current') fetch('http://127.0.0.1:8765/current')
@ -1181,12 +1259,7 @@ function fetchCurrent() {
document.getElementById('albumArtImg').src = document.getElementById('albumArtImg').src =
'http://127.0.0.1:8765/art?' + Date.now(); 'http://127.0.0.1:8765/art?' + Date.now();
} }
const line = d.lyric_line || ''; updateLyricContext(d.lyric_context || []);
if (line !== lastLyricLine) {
lastLyricLine = line;
lyricLineEl.textContent = line;
}
lyricDisplay.style.opacity = (isPlaying && line) ? '1' : '0';
}) })
.catch(() => {}); .catch(() => {});
} }
@ -1246,7 +1319,11 @@ function updateMusic(artist, title) {
document.querySelector('.robot-wrap').style.opacity = playing ? '1' : '0'; document.querySelector('.robot-wrap').style.opacity = playing ? '1' : '0';
document.querySelector('.music-ticker').style.opacity = playing ? '1' : '0'; document.querySelector('.music-ticker').style.opacity = playing ? '1' : '0';
albumArtEl.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(); if (playing) fetchCurrent();
const key = artist + '|' + title; const key = artist + '|' + title;
if (el.dataset.key === key) return; if (el.dataset.key === key) return;

View file

@ -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 "||"

View file

@ -38,7 +38,7 @@ WallpaperItem {
Plasma5Support.DataSource { Plasma5Support.DataSource {
id: musicSource id: musicSource
engine: "executable" 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 interval: 2000
onNewData: function(sourceName, data) { onNewData: function(sourceName, data) {