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