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:
parent
1a0f8c7ea5
commit
10b3bbbf2e
3 changed files with 104 additions and 19 deletions
|
|
@ -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 {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lyrics display -->
|
||||
<!-- Lyrics display — 5-line karaoke context -->
|
||||
<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>
|
||||
|
||||
<!-- Scanlines overlay -->
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue