refactor(theme): refactor theme.js

- Added runPlayerAction wrapper function to eliminate duplicate
Spicetify player command try/catches.
- Added runGlitchByDist helper function to dry up distance calculation
logic across glitch animations (burstGlitch, pulseGlitch,
implosionGlitch, fuzzWaveGlitch).
This commit is contained in:
Damjan Antevski
2026-08-05 11:10:22 +02:00
parent 46899bff25
commit 48863986a3
+132 -377
View File
@@ -43,7 +43,10 @@ const style = `
transition: top 0.5s cubic-bezier(0.4, 0, 0.2, 1), transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1); transition: top 0.5s cubic-bezier(0.4, 0, 0.2, 1), transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
} }
body.spotui-lyrics-panel #spotui-logo { body.spotui-lyrics-panel #spotui-logo,
body.spotui-playlist-panel #spotui-logo,
body.spotui-help-panel #spotui-logo,
body.spotui-about-panel #spotui-logo {
top: 12px; top: 12px;
transform: translate(-50%, 0) scale(0.6); transform: translate(-50%, 0) scale(0.6);
opacity: 0.8; opacity: 0.8;
@@ -103,21 +106,13 @@ body.spotui-lyrics-panel #spotui-top-fade {
} }
@media (max-width: 700px) { @media (max-width: 700px) {
.spotui-ascii-grid { .spotui-ascii-grid, .spotui-ascii-char {
font-size: clamp(5px, 1.1vw, 11px);
}
.spotui-ascii-char {
font-size: clamp(5px, 1.1vw, 11px); font-size: clamp(5px, 1.1vw, 11px);
} }
} }
@media (max-width: 450px) { @media (max-width: 450px) {
.spotui-ascii-grid { .spotui-ascii-grid, .spotui-ascii-char {
font-size: clamp(3.5px, 1.4vw, 7px);
}
.spotui-ascii-char {
font-size: clamp(3.5px, 1.4vw, 7px); font-size: clamp(3.5px, 1.4vw, 7px);
} }
} }
@@ -137,7 +132,11 @@ body.spotui-lyrics-panel #spotui-top-fade {
transition: opacity 260ms ease, transform 260ms ease; transition: opacity 260ms ease, transform 260ms ease;
} }
body.spotui-command-mode #spotui-output { body.spotui-command-mode #spotui-output,
body.spotui-playlist-panel #spotui-output,
body.spotui-help-panel #spotui-output,
body.spotui-about-panel #spotui-output,
body.spotui-lyrics-panel #spotui-output {
display: none !important; display: none !important;
} }
@@ -145,7 +144,12 @@ body.spotui-cli-mode #spotui-output {
display: flex !important; display: flex !important;
} }
#spotui-output::-webkit-scrollbar { #spotui-output::-webkit-scrollbar,
#spotui-help-panel::-webkit-scrollbar,
#spotui-about-panel::-webkit-scrollbar,
#spotui-playlist-list::-webkit-scrollbar,
#spotui-song-list::-webkit-scrollbar,
.spotui-lyrics-lines::-webkit-scrollbar {
width: 0; width: 0;
height: 0; height: 0;
} }
@@ -178,10 +182,6 @@ body.spotui-cli-mode #spotui-output {
.result { padding: 5px; } .result { padding: 5px; }
.selected { background: #ff8c42; color: #000; } .selected { background: #ff8c42; color: #000; }
body.spotui-lyrics-panel #spotui-output {
display: none !important;
}
body.spotui-lyrics-panel #spotui-logo { body.spotui-lyrics-panel #spotui-logo {
display: flex !important; display: flex !important;
} }
@@ -257,11 +257,6 @@ body.spotui-lyrics-panel #spotui-lyrics.spotui-lyrics-active {
text-align: center; text-align: center;
} }
.spotui-lyrics-lines::-webkit-scrollbar {
width: 0;
height: 0;
}
.spotui-lyrics-fade { .spotui-lyrics-fade {
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
@@ -333,19 +328,7 @@ body.spotui-playlist-panel #spotui-playlist-panel {
transition-delay: 0.6s; transition-delay: 0.6s;
} }
body.spotui-playlist-panel #spotui-logo { #spotui-help-panel, #spotui-about-panel {
top: 12px;
transform: translate(-50%, 0) scale(0.6);
opacity: 0.8;
z-index: 2;
background-color: #000;
}
body.spotui-playlist-panel #spotui-output, body.spotui-help-panel #spotui-output, body.spotui-about-panel #spotui-output {
display: none !important;
}
#spotui-help-panel {
display: none; display: none;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
@@ -360,56 +343,11 @@ body.spotui-playlist-panel #spotui-output, body.spotui-help-panel #spotui-output
background: #000; background: #000;
} }
#spotui-help-panel::-webkit-scrollbar { body.spotui-help-panel #spotui-help-panel,
width: 0;
height: 0;
}
body.spotui-help-panel #spotui-help-panel {
display: flex;
}
body.spotui-help-panel #spotui-logo {
top: 12px;
transform: translate(-50%, 0) scale(0.6);
opacity: 0.8;
z-index: 2;
background-color: #000;
}
#spotui-about-panel {
display: none;
flex: 1 1 auto;
flex-direction: column;
padding: 20px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
margin: 33vh 5vw 8px;
height: 60vh;
border: 1px solid #ff8c42;
border-radius: 4px;
background: #000;
}
#spotui-about-panel::-webkit-scrollbar {
width: 0;
height: 0;
}
body.spotui-about-panel #spotui-about-panel { body.spotui-about-panel #spotui-about-panel {
display: flex; display: flex;
} }
body.spotui-about-panel #spotui-logo {
top: 12px;
transform: translate(-50%, 0) scale(0.6);
opacity: 0.8;
z-index: 2;
background-color: #000;
}
.help-item { .help-item {
padding: 4px 0; padding: 4px 0;
display: flex; display: flex;
@@ -426,8 +364,6 @@ body.spotui-about-panel #spotui-logo {
color: #b3b3b3; color: #b3b3b3;
} }
#spotui-playlist-list, #spotui-song-list { #spotui-playlist-list, #spotui-song-list {
width: 50%; width: 50%;
overflow-y: auto; overflow-y: auto;
@@ -443,11 +379,6 @@ body.spotui-about-panel #spotui-logo {
padding: 0 5px; padding: 0 5px;
} }
#spotui-playlist-list::-webkit-scrollbar, #spotui-song-list::-webkit-scrollbar {
width: 0;
height: 0;
}
.playlist-item, .song-item { .playlist-item, .song-item {
padding: 4px 6px; padding: 4px 6px;
cursor: pointer; cursor: pointer;
@@ -458,7 +389,6 @@ body.spotui-about-panel #spotui-logo {
color: #000; color: #000;
} }
.spotui-lyrics-lines.unsynced .spotui-lyrics-line { .spotui-lyrics-lines.unsynced .spotui-lyrics-line {
color: #b3b3b3; color: #b3b3b3;
opacity: 0.9; opacity: 0.9;
@@ -679,7 +609,7 @@ function initAsciiAnimation() {
}); });
} }
async function burstGlitch(duration = 800) { async function runGlitchByDist(duration, logic) {
const centerRow = Math.floor(rows / 2); const centerRow = Math.floor(rows / 2);
const centerCol = Math.floor(cols / 2); const centerCol = Math.floor(cols / 2);
const withDist = charData.map((entry) => { const withDist = charData.map((entry) => {
@@ -688,8 +618,14 @@ function initAsciiAnimation() {
return { ...entry, dist: Math.sqrt(dr * dr + dc * dc) }; return { ...entry, dist: Math.sqrt(dr * dr + dc * dc) };
}); });
const maxDist = Math.max(...withDist.map((entry) => entry.dist), 1); const maxDist = Math.max(...withDist.map((entry) => entry.dist), 1);
await logic(withDist, maxDist);
resetGrid();
}
async function burstGlitch(duration = 800) {
const chars = [...GLITCH_CHARS]; const chars = [...GLITCH_CHARS];
const steps = 8; const steps = 8;
await runGlitchByDist(duration, async (withDist, maxDist) => {
for (let step = 0; step < steps; step += 1) { for (let step = 0; step < steps; step += 1) {
const progress = step / steps; const progress = step / steps;
withDist.forEach(({ el, original, color, dist }) => { withDist.forEach(({ el, original, color, dist }) => {
@@ -697,8 +633,7 @@ function initAsciiAnimation() {
const threshold = progress * 1.1; const threshold = progress * 1.1;
if (norm < threshold + 0.12 && norm > threshold - 0.12) { if (norm < threshold + 0.12 && norm > threshold - 0.12) {
if (Math.random() < 0.75) { if (Math.random() < 0.75) {
const randIdx = Math.floor(Math.random() * chars.length); el.textContent = chars[Math.floor(Math.random() * chars.length)];
el.textContent = chars[randIdx];
el.style.color = `hsl(${20 + Math.random() * 35}, 100%, ${50 + Math.random() * 30}%)`; el.style.color = `hsl(${20 + Math.random() * 35}, 100%, ${50 + Math.random() * 30}%)`;
} }
} else if (norm < threshold - 0.12) { } else if (norm < threshold - 0.12) {
@@ -708,22 +643,14 @@ function initAsciiAnimation() {
}); });
await sleep(Math.floor(duration / steps)); await sleep(Math.floor(duration / steps));
} }
resetGrid(); });
} }
async function pulseGlitch(duration = 1200) { async function pulseGlitch(duration = 1200) {
const centerRow = Math.floor(rows / 2);
const centerCol = Math.floor(cols / 2);
const withDist = charData.map((entry) => {
const dr = entry.row - centerRow;
const dc = entry.col - centerCol;
return { ...entry, dist: Math.sqrt(dr * dr + dc * dc) };
});
const maxDist = Math.max(...withDist.map((entry) => entry.dist), 1);
const chars = [...GLITCH_CHARS]; const chars = [...GLITCH_CHARS];
const waves = 3; const waves = 3;
const stepsPerWave = 10; const stepsPerWave = 10;
await runGlitchByDist(duration, async (withDist, maxDist) => {
for (let wave = 0; wave < waves; wave += 1) { for (let wave = 0; wave < waves; wave += 1) {
for (let step = 0; step < stepsPerWave; step += 1) { for (let step = 0; step < stepsPerWave; step += 1) {
const progress = step / stepsPerWave; const progress = step / stepsPerWave;
@@ -745,27 +672,18 @@ function initAsciiAnimation() {
} }
await sleep(40); await sleep(40);
} }
resetGrid(); });
} }
async function implosionGlitch(duration = 900) { async function implosionGlitch(duration = 900) {
const centerRow = Math.floor(rows / 2);
const centerCol = Math.floor(cols / 2);
const withDist = charData.map((entry) => {
const dr = entry.row - centerRow;
const dc = entry.col - centerCol;
return { ...entry, dist: Math.sqrt(dr * dr + dc * dc) };
});
const maxDist = Math.max(...withDist.map((entry) => entry.dist), 1);
const chars = [...GLITCH_CHARS]; const chars = [...GLITCH_CHARS];
const steps = 10; const steps = 10;
await runGlitchByDist(duration, async (withDist, maxDist) => {
withDist.forEach(({ el }) => { withDist.forEach(({ el }) => {
const randIdx = Math.floor(Math.random() * chars.length); const randIdx = Math.floor(Math.random() * chars.length);
el.textContent = chars[randIdx]; el.textContent = chars[randIdx];
el.style.color = `hsl(${20 + Math.random() * 35}, 100%, ${45 + Math.random() * 35}%)`; el.style.color = `hsl(${20 + Math.random() * 35}, 100%, ${45 + Math.random() * 35}%)`;
}); });
for (let step = 0; step < steps; step += 1) { for (let step = 0; step < steps; step += 1) {
const progress = step / steps; const progress = step / steps;
const threshold = 1.0 - progress * 1.1; const threshold = 1.0 - progress * 1.1;
@@ -778,7 +696,7 @@ function initAsciiAnimation() {
}); });
await sleep(Math.floor(duration / steps)); await sleep(Math.floor(duration / steps));
} }
resetGrid(); });
} }
async function spiralGlitch(duration = 1000) { async function spiralGlitch(duration = 1000) {
@@ -815,18 +733,10 @@ function initAsciiAnimation() {
} }
async function fuzzWaveGlitch(duration = 1000) { async function fuzzWaveGlitch(duration = 1000) {
const centerRow = Math.floor(rows / 2);
const centerCol = Math.floor(cols / 2);
const withDist = charData.map((entry) => {
const dr = entry.row - centerRow;
const dc = entry.col - centerCol;
return { ...entry, dist: Math.sqrt(dr * dr + dc * dc) };
});
const maxDist = Math.max(...withDist.map((entry) => entry.dist), 1);
const chars = [...GLITCH_CHARS]; const chars = [...GLITCH_CHARS];
const steps = 20; const steps = 20;
const bandWidth = 0.25; const bandWidth = 0.25;
await runGlitchByDist(duration, async (withDist, maxDist) => {
for (let step = 0; step < steps; step += 1) { for (let step = 0; step < steps; step += 1) {
const progress = step / steps; const progress = step / steps;
const targetNorm = progress * 1.0; const targetNorm = progress * 1.0;
@@ -844,7 +754,7 @@ function initAsciiAnimation() {
}); });
await sleep(Math.floor(duration / steps)); await sleep(Math.floor(duration / steps));
} }
resetGrid(); });
} }
async function staticGlitch(duration = 600) { async function staticGlitch(duration = 600) {
@@ -928,45 +838,16 @@ function initAsciiAnimation() {
} }
} }
async function stageBurst() { async function stageBurst() { await burstGlitch(900); }
await burstGlitch(900); async function stagePulse() { await pulseGlitch(1200); }
} async function stageImplosion() { await implosionGlitch(900); }
async function stageSpiral() { await spiralGlitch(1000); }
async function stagePulse() { async function stageFuzzWave() { await fuzzWaveGlitch(1000); }
await pulseGlitch(1200); async function stageStatic() { await staticGlitch(700); }
} async function stageHSlashDown() { await horizontalBand(1, 800); }
async function stageHSlashUp() { await horizontalBand(-1, 800); }
async function stageImplosion() { async function stageVSlashRight() { await verticalSlice(1, 800); }
await implosionGlitch(900); async function stageVSlashLeft() { await verticalSlice(-1, 800); }
}
async function stageSpiral() {
await spiralGlitch(1000);
}
async function stageFuzzWave() {
await fuzzWaveGlitch(1000);
}
async function stageStatic() {
await staticGlitch(700);
}
async function stageHSlashDown() {
await horizontalBand(1, 800);
}
async function stageHSlashUp() {
await horizontalBand(-1, 800);
}
async function stageVSlashRight() {
await verticalSlice(1, 800);
}
async function stageVSlashLeft() {
await verticalSlice(-1, 800);
}
const stageFunctions = [ const stageFunctions = [
stageWaveDown, stageWaveDown,
@@ -1008,7 +889,6 @@ function initAsciiAnimation() {
} }
let tuiMode = "command"; let tuiMode = "command";
let results = []; let results = [];
let selected = 0; let selected = 0;
let lyricsObserver = null; let lyricsObserver = null;
@@ -1036,8 +916,7 @@ function createCopyButton() {
copyBtn.addEventListener("click", () => { copyBtn.addEventListener("click", () => {
const output = document.getElementById("spotui-output"); const output = document.getElementById("spotui-output");
if (output) { if (output) {
const text = output.innerText; navigator.clipboard.writeText(output.innerText).then(() => {
navigator.clipboard.writeText(text).then(() => {
copyBtn.textContent = "Copied!"; copyBtn.textContent = "Copied!";
setTimeout(() => { copyBtn.textContent = "Copy log"; }, 1500); setTimeout(() => { copyBtn.textContent = "Copy log"; }, 1500);
}).catch(() => { }).catch(() => {
@@ -1076,17 +955,14 @@ function createCopyButton() {
controls.appendChild(copyBtn); controls.appendChild(copyBtn);
controls.appendChild(hideBtn); controls.appendChild(hideBtn);
controls.appendChild(spotifyBtn); controls.appendChild(spotifyBtn);
const footer = document.getElementById("spotui-footer"); (document.getElementById("spotui-footer") || document.body).appendChild(controls);
(footer || document.body).appendChild(controls);
const backBtn = document.createElement("button"); const backBtn = document.createElement("button");
backBtn.id = "spotui-back-btn"; backBtn.id = "spotui-back-btn";
backBtn.className = "spotui-control-btn"; backBtn.className = "spotui-control-btn";
backBtn.textContent = "Back"; backBtn.textContent = "Back";
backBtn.addEventListener("click", () => { backBtn.addEventListener("click", () => {
document.body.classList.remove("spotui-search-mode"); document.body.classList.remove("spotui-search-mode", "spotui-spotify-enabled", "spotui-tui-hidden");
document.body.classList.remove("spotui-spotify-enabled");
document.body.classList.remove("spotui-tui-hidden");
spotifyBtn.textContent = "Enable Spotify"; spotifyBtn.textContent = "Enable Spotify";
hideBtn.textContent = "Hide TUI"; hideBtn.textContent = "Hide TUI";
syncLyricsState(); syncLyricsState();
@@ -1103,9 +979,10 @@ function detectLyricsSurface() {
} }
function syncLyricsState() { function syncLyricsState() {
if (!document.body) return; if (document.body) {
document.body.classList.toggle("spotui-lyrics-open", detectLyricsSurface()); document.body.classList.toggle("spotui-lyrics-open", detectLyricsSurface());
} }
}
function hookLyricsButton() { function hookLyricsButton() {
const button = document.querySelector(".main-nowPlayingBar-lyricsButton"); const button = document.querySelector(".main-nowPlayingBar-lyricsButton");
@@ -1127,14 +1004,11 @@ function initLyricsBridge() {
setTimeout(initLyricsBridge, 250); setTimeout(initLyricsBridge, 250);
return; return;
} }
const refresh = () => { const refresh = () => {
hookLyricsButton(); hookLyricsButton();
syncLyricsState(); syncLyricsState();
}; };
refresh(); refresh();
if (!lyricsObserver) { if (!lyricsObserver) {
lyricsObserver = new MutationObserver(refresh); lyricsObserver = new MutationObserver(refresh);
lyricsObserver.observe(document.body, { lyricsObserver.observe(document.body, {
@@ -1145,9 +1019,7 @@ function initLyricsBridge() {
}); });
window.addEventListener( window.addEventListener(
"beforeunload", "beforeunload",
() => { () => { lyricsObserver?.disconnect(); },
lyricsObserver?.disconnect();
},
{ once: true } { once: true }
); );
} }
@@ -1206,7 +1078,6 @@ function createTerminal() {
renderResults(); renderResults();
} }
}); });
} }
function print(text) { function print(text) {
@@ -1219,6 +1090,15 @@ function print(text) {
output.scrollTop = 0; output.scrollTop = 0;
} }
async function runPlayerAction(actionName, promiseFn, successMsg) {
try {
const res = await promiseFn();
print(successMsg ?? `Executed ${actionName}`);
} catch (err) {
print(`${actionName} error: ${err.message}`);
}
}
async function execute(cmd) { async function execute(cmd) {
const rawCmd = cmd.trim(); const rawCmd = cmd.trim();
const cleanedCmd = rawCmd.startsWith("/") || rawCmd.startsWith(".") ? rawCmd.slice(1).trim() : rawCmd; const cleanedCmd = rawCmd.startsWith("/") || rawCmd.startsWith(".") ? rawCmd.slice(1).trim() : rawCmd;
@@ -1240,167 +1120,91 @@ async function execute(cmd) {
return; return;
} }
if (command === "help") { if (command === "help") { openHelpPanel(); return; }
openHelpPanel(); if (command === "about") { openAboutPanel(); return; }
return; if (command === "clear") { document.getElementById("spotui-output").textContent = ""; return; }
} if (command === "playlist" || command === "list") { openPlaylistPanel(); return; }
if (command === "about") {
openAboutPanel();
return;
}
if (command === "clear") {
document.getElementById("spotui-output").textContent = "";
return;
}
if (command === "playlist") {
openPlaylistPanel();
return;
}
if (command === "list") {
openPlaylistPanel();
return;
}
if (command === "play") { if (command === "play") {
try { await runPlayerAction("Play", async () => {
const wasPlaying = Spicetify.Player.isPlaying(); if (!Spicetify.Player.isPlaying()) Spicetify.Player.togglePlay();
if (!wasPlaying) { }, "Playing");
Spicetify.Player.togglePlay();
}
print("Playing");
} catch (err) {
print("Play error: " + err.message);
}
return; return;
} }
if (command === "pause") { if (command === "pause") {
try { await runPlayerAction("Pause", async () => {
const wasPlaying = Spicetify.Player.isPlaying(); if (Spicetify.Player.isPlaying()) Spicetify.Player.togglePlay();
if (wasPlaying) { }, "Paused");
Spicetify.Player.togglePlay();
}
print("Paused");
} catch (err) {
print("Pause error: " + err.message);
}
return; return;
} }
if (command === "p") { if (command === "p") {
try { await runPlayerAction("Play/PauseToggle", async () => {
const wasPlaying = Spicetify.Player.isPlaying(); const wasPlaying = Spicetify.Player.isPlaying();
Spicetify.Player.togglePlay(); Spicetify.Player.togglePlay();
print(wasPlaying ? "Paused" : "Playing"); return wasPlaying;
} catch (err) { }, Spicetify.Player.isPlaying() ? "Paused" : "Playing");
print("Play/pause error: " + err.message);
}
return; return;
} }
if (command === "search") { if (command === "search") {
document.body.classList.add("spotui-search-mode"); document.body.classList.add("spotui-search-mode", "spotui-tui-hidden");
document.body.classList.add("spotui-tui-hidden");
syncLyricsState(); syncLyricsState();
return; return;
} }
if (command === "skip") { if (command === "skip") {
try { await runPlayerAction("Skip", () => Spicetify.Player.next(), "Skipped to next track");
Spicetify.Player.next();
print("Skipped to next track");
} catch (err) {
print("Skip error: " + err.message);
}
return; return;
} }
if (command === "back") { if (command === "back") {
try { await runPlayerAction("Back", () => Spicetify.Player.back(), "Went back to previous track");
Spicetify.Player.back();
print("Went back to previous track");
} catch (err) {
print("Back error: " + err.message);
}
return; return;
} }
if (command === "seek" || command === "s") { if (command === "seek" || command === "s") {
try { await runPlayerAction("Seek", () => {
if (!argText) { if (!argText) throw new Error("Usage: seek <mm:ss>");
print("Usage: seek <mm:ss>");
return;
}
const parts = argText.split(':').map(Number); const parts = argText.split(':').map(Number);
if (parts.length !== 2 || parts.some(isNaN)) { if (parts.length !== 2 || parts.some(isNaN)) throw new Error("Invalid time format. Use mm:ss.");
print("Invalid time format. Use mm:ss."); Spicetify.Player.seek((parts[0] * 60 + parts[1]) * 1000);
return; }, `Seeked to ${argText}`);
}
const seekMs = (parts[0] * 60 + parts[1]) * 1000;
Spicetify.Player.seek(seekMs);
print(`Seeked to ${argText}`);
} catch (err) {
print("Seek error: " + err.message);
}
return; return;
} }
if (command === "volume" || command === "v") { if (command === "volume" || command === "v") {
try { await runPlayerAction("Volume", () => {
if (!argText) { if (!argText) throw new Error("Usage: volume <0-100>");
print("Usage: volume <0-100>");
return;
}
const percent = Number(argText); const percent = Number(argText);
if (!Number.isFinite(percent) || percent < 0 || percent > 100) { if (!Number.isFinite(percent) || percent < 0 || percent > 100) throw new Error("Volume must be between 0 and 100.");
print("Volume must be between 0 and 100.");
return;
}
Spicetify.Player.setVolume(percent / 100); Spicetify.Player.setVolume(percent / 100);
print(`Volume is ${Math.round(percent)}%`); }, `Volume is ${Math.round(Number(argText))}%`);
} catch (err) {
print("Volume error: " + err.message);
}
return; return;
} }
if (command === "shuffle") { if (command === "shuffle") {
try { await runPlayerAction("Shuffle", () => {
const current = Spicetify.Player.getShuffle(); const current = Spicetify.Player.getShuffle();
Spicetify.Player.setShuffle(!current); Spicetify.Player.setShuffle(!current);
print("Shuffle: " + (!current ? "ON" : "OFF")); return current;
} catch (err) { }, "Shuffle: " + (!Spicetify.Player.getShuffle() ? "ON" : "OFF"));
print("Shuffle error: " + err.message);
}
return; return;
} }
if (command === "loop") { if (command === "loop") { handleRepeatCommand("loop", argText); return; }
handleRepeatCommand("loop", argText); if (command === "superloop") { handleRepeatCommand("superloop", argText); return; }
return;
}
if (command === "superloop") {
handleRepeatCommand("superloop", argText);
return;
}
if (command === "like") { if (command === "like") {
try { await runPlayerAction("Like", async () => {
const isLiked = await Spicetify.Player.getHeart(); const isLiked = await Spicetify.Player.getHeart();
await Spicetify.Player.toggleHeart(); await Spicetify.Player.toggleHeart();
print(!isLiked ? "Liked song" : "Unliked song"); return isLiked;
} catch (err) { }, Spicetify.Player.getHeart() ? "Unliked song" : "Liked song");
print("Like error: " + err.message);
}
return; return;
} }
if (command === "lyrics") { if (command === "lyrics") { handleLyricsCommand(argText); return; }
handleLyricsCommand(argText);
return;
}
print("Unknown command. Type /help"); print("Unknown command. Type /help");
} }
@@ -1452,70 +1256,52 @@ function handleGlobalEsc(e) {
} }
} }
function closeHelpPanel() { function setPanelState(panelId, className, openVarName, targetState) {
if (!helpPanelOpen) return; const panels = {
helpPanelOpen = false; 'helpPanelOpen': () => helpPanelOpen = targetState,
document.body.classList.remove("spotui-help-panel"); 'aboutPanelOpen': () => aboutPanelOpen = targetState
const panel = document.getElementById("spotui-help-panel"); };
if (panel) { if (panels[openVarName]) panels[openVarName]();
panel.hidden = true; document.body.classList.toggle(className, targetState);
} const panel = document.getElementById(panelId);
if (panel) panel.hidden = !targetState;
const input = document.getElementById("spotui-input"); const input = document.getElementById("spotui-input");
if (input) input.focus(); if (input) {
document.removeEventListener("keydown", handleGlobalEsc); if (targetState) input.blur();
else input.focus();
}
if (targetState) document.addEventListener("keydown", handleGlobalEsc);
else document.removeEventListener("keydown", handleGlobalEsc);
} }
function closeHelpPanel() { if (helpPanelOpen) setPanelState("spotui-help-panel", "spotui-help-panel", "helpPanelOpen", false); }
function openHelpPanel() { function openHelpPanel() {
if (helpPanelOpen) { if (helpPanelOpen) { closeHelpPanel(); return; }
closeHelpPanel();
return;
}
if (lyricsPanelOpen) closeLyricsPanel(); if (lyricsPanelOpen) closeLyricsPanel();
if (playlistPanelOpen) closePlaylistPanel(); if (playlistPanelOpen) closePlaylistPanel();
if (aboutPanelOpen) closeAboutPanel(); if (aboutPanelOpen) closeAboutPanel();
helpPanelOpen = true; setPanelState("spotui-help-panel", "spotui-help-panel", "helpPanelOpen", true);
document.body.classList.add("spotui-help-panel");
document.addEventListener("keydown", handleGlobalEsc);
const panel = document.getElementById("spotui-help-panel"); const panel = document.getElementById("spotui-help-panel");
if (panel) { if (panel) {
panel.hidden = false;
panel.innerHTML = COMMAND_LIST.map( panel.innerHTML = COMMAND_LIST.map(
item => `<div class="help-item"><span class="command">${item.cmd}</span><span class="description">${item.desc}</span></div>` item => `<div class="help-item"><span class="command">${item.cmd}</span><span class="description">${item.desc}</span></div>`
).join(''); ).join('');
} }
const input = document.getElementById("spotui-input");
if (input) input.blur();
} }
function closeAboutPanel() { function closeAboutPanel() { if (aboutPanelOpen) setPanelState("spotui-about-panel", "spotui-about-panel", "aboutPanelOpen", false); }
if (!aboutPanelOpen) return;
aboutPanelOpen = false;
document.body.classList.remove("spotui-about-panel");
const panel = document.getElementById("spotui-about-panel");
if (panel) {
panel.hidden = true;
}
const input = document.getElementById("spotui-input");
if (input) input.focus();
document.removeEventListener("keydown", handleGlobalEsc);
}
function openAboutPanel() { function openAboutPanel() {
if (aboutPanelOpen) { if (aboutPanelOpen) { closeAboutPanel(); return; }
closeAboutPanel();
return;
}
if (lyricsPanelOpen) closeLyricsPanel(); if (lyricsPanelOpen) closeLyricsPanel();
if (playlistPanelOpen) closePlaylistPanel(); if (playlistPanelOpen) closePlaylistPanel();
if (helpPanelOpen) closeHelpPanel(); if (helpPanelOpen) closeHelpPanel();
aboutPanelOpen = true; setPanelState("spotui-about-panel", "spotui-about-panel", "aboutPanelOpen", true);
document.body.classList.add("spotui-about-panel");
document.addEventListener("keydown", handleGlobalEsc);
const panel = document.getElementById("spotui-about-panel"); const panel = document.getElementById("spotui-about-panel");
if (panel) { if (panel) {
panel.hidden = false;
panel.innerHTML = ` panel.innerHTML = `
<div class="help-item"><span class="command">Developer</span><span class="description">SkenS</span></div> <div class="help-item"><span class="command">Developer</span><span class="description">SkenS</span></div>
<div class="help-item"><span class="command">Repository</span><span class="description"><a href="https://github.com/SkenSMasteR/SpoTUI">https://github.com/SkenSMasteR/SpoTUI</a></span></div> <div class="help-item"><span class="command">Repository</span><span class="description"><a href="https://github.com/SkenSMasteR/SpoTUI">https://github.com/SkenSMasteR/SpoTUI</a></span></div>
@@ -1523,29 +1309,20 @@ function openAboutPanel() {
<div class="help-item"><span class="command">Contact</span><span class="description"><a href="mailto:receive@gmx.us">receive@gmx.us</a></span></div> <div class="help-item"><span class="command">Contact</span><span class="description"><a href="mailto:receive@gmx.us">receive@gmx.us</a></span></div>
`; `;
} }
const input = document.getElementById("spotui-input");
if (input) input.blur();
} }
function closePlaylistPanel() { function closePlaylistPanel() {
playlistPanelOpen = false; playlistPanelOpen = false;
document.body.classList.remove("spotui-playlist-panel"); document.body.classList.remove("spotui-playlist-panel");
const panel = document.getElementById("spotui-playlist-panel"); const panel = document.getElementById("spotui-playlist-panel");
if (panel) { if (panel) panel.hidden = true;
panel.hidden = true;
}
const input = document.getElementById("spotui-input"); const input = document.getElementById("spotui-input");
if (input) input.focus(); if (input) input.focus();
document.removeEventListener("keydown", handlePlaylistPanelKeydown); document.removeEventListener("keydown", handlePlaylistPanelKeydown);
} }
async function openPlaylistPanel() { async function openPlaylistPanel() {
if (playlistPanelOpen) { if (playlistPanelOpen) { closePlaylistPanel(); return; }
closePlaylistPanel();
return;
}
if (lyricsPanelOpen) closeLyricsPanel(); if (lyricsPanelOpen) closeLyricsPanel();
if (helpPanelOpen) closeHelpPanel(); if (helpPanelOpen) closeHelpPanel();
@@ -1559,9 +1336,7 @@ async function openPlaylistPanel() {
playlistPanelOpen = true; playlistPanelOpen = true;
document.body.classList.add("spotui-playlist-panel"); document.body.classList.add("spotui-playlist-panel");
const panel = document.getElementById("spotui-playlist-panel"); const panel = document.getElementById("spotui-playlist-panel");
if (panel) { if (panel) panel.hidden = false;
panel.hidden = false;
}
const input = document.getElementById("spotui-input"); const input = document.getElementById("spotui-input");
if (input) input.blur(); if (input) input.blur();
@@ -1577,16 +1352,12 @@ async function openPlaylistPanel() {
async function renderPlaylistPanel() { async function renderPlaylistPanel() {
const playlistList = document.getElementById("spotui-playlist-list"); const playlistList = document.getElementById("spotui-playlist-list");
const songList = document.getElementById("spotui-song-list"); const songList = document.getElementById("spotui-song-list");
if (!playlistList || !songList) return; if (!playlistList || !songList) return;
playlistList.innerHTML = ""; playlistList.innerHTML = "";
playlists.forEach((p, idx) => { playlists.forEach((p, idx) => {
const item = document.createElement("div"); const item = document.createElement("div");
item.className = "playlist-item"; item.className = "playlist-item" + (idx === selectedPlaylist ? " selected" : "");
if (idx === selectedPlaylist) {
item.classList.add("selected");
}
item.textContent = p.name; item.textContent = p.name;
playlistList.appendChild(item); playlistList.appendChild(item);
}); });
@@ -1608,10 +1379,7 @@ async function renderPlaylistPanel() {
songList.innerHTML = ""; songList.innerHTML = "";
playlistSongs.forEach((s, idx) => { playlistSongs.forEach((s, idx) => {
const item = document.createElement("div"); const item = document.createElement("div");
item.className = "song-item"; item.className = "song-item" + (idx === selectedSong && activePane === 'song' ? " selected" : "");
if (idx === selectedSong && activePane === 'song') {
item.classList.add("selected");
}
item.textContent = `${s.name} - ${s.artist}`; item.textContent = `${s.name} - ${s.artist}`;
songList.appendChild(item); songList.appendChild(item);
}); });
@@ -1621,9 +1389,7 @@ async function renderPlaylistPanel() {
function scrollSelectedIntoView() { function scrollSelectedIntoView() {
const selectedItem = document.querySelector(activePane === 'playlist' ? '.playlist-item.selected' : '.song-item.selected'); const selectedItem = document.querySelector(activePane === 'playlist' ? '.playlist-item.selected' : '.song-item.selected');
if (selectedItem) { if (selectedItem) selectedItem.scrollIntoView({ block: 'nearest' });
selectedItem.scrollIntoView({ block: 'nearest' });
}
} }
async function handlePlaylistPanelKeydown(e) { async function handlePlaylistPanelKeydown(e) {
@@ -1678,7 +1444,6 @@ async function handlePlaylistPanelKeydown(e) {
} }
} }
function getTrackTitle(track, index = 0) { function getTrackTitle(track, index = 0) {
const meta = track?.metadata || track?.contextTrack?.metadata || {}; const meta = track?.metadata || track?.contextTrack?.metadata || {};
return track?.name || track?.title || meta.title || meta.name || `Track ${index + 1}`; return track?.name || track?.title || meta.title || meta.name || `Track ${index + 1}`;
@@ -1740,16 +1505,10 @@ function handleRepeatCommand(kind, arg) {
const targetMode = kind === "loop" ? 1 : 2; const targetMode = kind === "loop" ? 1 : 2;
let nextMode = targetMode; let nextMode = targetMode;
if (arg === "on") { if (arg === "on") nextMode = targetMode;
nextMode = targetMode; else if (arg === "off") nextMode = 0;
} else if (arg === "off") { else if (arg === "") nextMode = current === targetMode ? 0 : targetMode;
nextMode = 0; else { print(`Usage: /${kind} [on|off]`); return; }
} else if (arg === "") {
nextMode = current === targetMode ? 0 : targetMode;
} else {
print(`Usage: /${kind} [on|off]`);
return;
}
Spicetify.Player.setRepeat(nextMode); Spicetify.Player.setRepeat(nextMode);
print(`${kind === "loop" ? "Loop" : "Superloop"}: ${nextMode === 0 ? "OFF" : "ON"}`); print(`${kind === "loop" ? "Loop" : "Superloop"}: ${nextMode === 0 ? "OFF" : "ON"}`);
@@ -1775,7 +1534,6 @@ async function getPlaylists() {
} }
const LYRICS_STORAGE_KEY = "spotui:lyrics-open"; const LYRICS_STORAGE_KEY = "spotui:lyrics-open";
let lyricsPanelOpen = false; let lyricsPanelOpen = false;
let lyricsLoadToken = 0; let lyricsLoadToken = 0;
let lyricsActiveIndex = -1; let lyricsActiveIndex = -1;
@@ -1866,7 +1624,7 @@ async function fetchLrclibLyrics(info) {
const result = normalizeLrclibPayload(data); const result = normalizeLrclibPayload(data);
if (result) return result; if (result) return result;
} }
} catch { /* try search */ } } catch { }
try { try {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
track_name: info.title, track_name: info.title,
@@ -1982,8 +1740,8 @@ async function loadLyricsForCurrentTrack() {
if (lyricsCache.uri === info.uri && (lyricsCache.lines.length || lyricsCache.instrumental || lyricsCache.error)) { if (lyricsCache.uri === info.uri && (lyricsCache.lines.length || lyricsCache.instrumental || lyricsCache.error)) {
setLyricsHeader(info, lyricsCache.instrumental ? "instrumental" : `${lyricsCache.synced ? "synced" : "unsynced"} · ${lyricsCache.provider || "cache"}`); setLyricsHeader(info, lyricsCache.instrumental ? "instrumental" : `${lyricsCache.synced ? "synced" : "unsynced"} · ${lyricsCache.provider || "cache"}`);
if (lyricsCache.instrumental) { renderLyricsEmpty("Instrumental", "No vocals to show for this track."); } if (lyricsCache.instrumental) renderLyricsEmpty("Instrumental", "No vocals to show for this track.");
else if (lyricsCache.error) { renderLyricsEmpty("No lyrics", lyricsCache.error); } else if (lyricsCache.error) renderLyricsEmpty("No lyrics", lyricsCache.error);
else { renderLyricsLines(lyricsCache.lines, lyricsCache.synced); syncLyricsHighlight(true); } else { renderLyricsLines(lyricsCache.lines, lyricsCache.synced); syncLyricsHighlight(true); }
return; return;
} }
@@ -2011,7 +1769,7 @@ async function loadLyricsForCurrentTrack() {
} }
function storeLyricsOpen(open) { function storeLyricsOpen(open) {
try { localStorage.setItem(LYRICS_STORAGE_KEY, open ? "1" : "0"); } catch { /* ignore */ } try { localStorage.setItem(LYRICS_STORAGE_KEY, open ? "1" : "0"); } catch { }
} }
function openLyricsPanel() { function openLyricsPanel() {
@@ -2077,16 +1835,13 @@ injectStyle();
setTimeout(createCopyButton, 500); setTimeout(createCopyButton, 500);
setTimeout(initLyricsBridge, 1000); setTimeout(initLyricsBridge, 1000);
if (Spicetify?.Platform) { if (Spicetify?.Platform) createTerminal();
createTerminal(); else setTimeout(createTerminal, 1500);
} else {
setTimeout(createTerminal, 1500);
}
try { try {
if (localStorage.getItem(LYRICS_STORAGE_KEY) === "1") { if (localStorage.getItem(LYRICS_STORAGE_KEY) === "1") {
setTimeout(() => openLyricsPanel(), 2000); setTimeout(() => openLyricsPanel(), 2000);
} }
} catch { /* ignore */ } } catch { }
})(); })();