diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html index 75b0ab3..0602200 100644 --- a/plugin/contents/ui/dancer.html +++ b/plugin/contents/ui/dancer.html @@ -237,29 +237,38 @@ html, body { } /* ============================================================ - LYRICS DISPLAY + LYRICS DISPLAY — verse karaoke ============================================================ */ .lyric-display { position: absolute; left: 2%; top: 4%; - max-width: 36%; + width: 38%; + height: 150px; /* 5 visible slots × 30px */ + overflow: hidden; pointer-events: none; opacity: 0; transition: opacity 0.5s ease; + -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%); + mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%); } -.lyric-line { - color: #ffffff; +.lyric-ctx { + position: absolute; + left: 0; + width: 100%; + height: 30px; + line-height: 30px; font-family: 'Courier New', monospace; - font-size: clamp(16px, 1.3vw, 18px); - line-height: 1.5; - letter-spacing: 0.03em; - 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)), - 0 2px 10px rgba(0,0,0,0.95), - -1px -1px 0 rgba(0,0,0,0.7), - 1px 1px 0 rgba(0,0,0,0.7); + letter-spacing: 0.02em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: + top 0.5s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.45s ease, + font-size 0.35s ease, + color 0.35s ease, + text-shadow 0.35s ease; } /* ============================================================ @@ -887,10 +896,8 @@ html, body { - -
-
-
+ +
@@ -1163,13 +1170,98 @@ function applyTheme(rgb) { }); } -const albumArtEl = document.getElementById('albumArt'); -const fogWisps = document.querySelectorAll('.fog-wisp'); +const albumArtEl = document.getElementById('albumArt'); +const fogWisps = document.querySelectorAll('.fog-wisp'); const lyricDisplay = document.getElementById('lyricDisplay'); -const lyricLineEl = document.getElementById('lyricLine'); -let lastArtUrl = ''; -let lastLyricLine = ''; +let lastArtUrl = ''; + +/* ── verse display ── */ +const SLOT_H = 30; +const CENTER_PX = 60; // top of the active slot within the 150px container + +const ACTIVE_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)),' + + '0 2px 10px rgba(0,0,0,0.95)'; + +const VERSE_STYLES = [ + { opacity:'0.2', fontSize:'clamp(11px,0.85vw,13px)', color:'#777', shadow:'' }, // dist -2 + { opacity:'0.48', fontSize:'clamp(13px,1.0vw,15px)', color:'#bbb', shadow:'' }, // dist -1 + { opacity:'1', fontSize:'clamp(16px,1.3vw,18px)', color:'#fff', shadow: ACTIVE_SHADOW }, // dist 0 + { opacity:'0.48', fontSize:'clamp(13px,1.0vw,15px)', color:'#bbb', shadow:'' }, // dist +1 + { opacity:'0.2', fontSize:'clamp(11px,0.85vw,13px)', color:'#777', shadow:'' }, // dist +2 +]; + +let verseEls = []; +let currentVerseKey = ''; +let currentVerseActive = -1; + +function applyVerseStyle(el, dist) { + if (dist < -2 || dist > 2) { + el.style.opacity = '0'; el.style.fontSize = 'clamp(11px,0.85vw,13px)'; + el.style.color = '#777'; el.style.textShadow = ''; return; + } + const p = VERSE_STYLES[dist + 2]; + el.style.opacity = p.opacity; + el.style.fontSize = p.fontSize; + el.style.color = p.color; + el.style.textShadow = p.shadow; +} + +function positionVerseEls(verse_active) { + verseEls.forEach((el, i) => { + const dist = i - verse_active; + el.style.top = (CENTER_PX + dist * SLOT_H) + 'px'; + applyVerseStyle(el, dist); + }); +} + +function buildVerse(verse_lines, verse_active) { + lyricDisplay.innerHTML = ''; + verseEls = verse_lines.map(text => { + const el = document.createElement('div'); + el.className = 'lyric-ctx'; + el.textContent = text; + el.style.transition = 'none'; + lyricDisplay.appendChild(el); + return el; + }); + positionVerseEls(verse_active); + requestAnimationFrame(() => requestAnimationFrame(() => { + verseEls.forEach(el => { el.style.transition = ''; }); + })); +} + +function updateLyricVerse(verse_lines, verse_active) { + if (!verse_lines || !verse_lines.length || verse_active < 0 || !isPlaying) { + lyricDisplay.style.opacity = '0'; + currentVerseKey = ''; + currentVerseActive = -1; + return; + } + const verseKey = verse_lines.join('\x00'); + + if (verseKey !== currentVerseKey) { + currentVerseKey = verseKey; + currentVerseActive = verse_active; + if (lyricDisplay.style.opacity === '1') { + lyricDisplay.style.opacity = '0'; + setTimeout(() => { + buildVerse(verse_lines, verse_active); + lyricDisplay.style.opacity = '1'; + }, 500); + } else { + buildVerse(verse_lines, verse_active); + lyricDisplay.style.opacity = '1'; + } + } else if (verse_active !== currentVerseActive) { + currentVerseActive = verse_active; + positionVerseEls(verse_active); + } else { + lyricDisplay.style.opacity = '1'; + } +} function fetchCurrent() { fetch('http://127.0.0.1:8765/current') @@ -1181,12 +1273,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'; + updateLyricVerse(d.verse_lines || [], d.verse_active ?? -1); }) .catch(() => {}); } @@ -1246,7 +1333,12 @@ 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'; + currentVerseKey = ''; + currentVerseActive = -1; + } 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) {