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) {