commit 8a9e1c144a0233bb1785c96a05643a93d49ac503 Author: Damjan Antevski Date: Mon Jul 13 22:26:37 2026 +0200 Initial SpoTUI package diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9db013 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# SpoTUI + +SpoTUI is a terminal-style Spotify skin built for Spicetify. + +It combines: +- a custom UI overlay in `theme.js` +- theme styling in `user.css` +- Spotify color values in `color.ini` + +## Screenshot + +![SpoTUI preview](preview.png) + +## Features + +- Terminal-inspired interface +- Built-in playback controls +- Playlist browsing and management +- Spotify-native fallback mode +- Copy-log and hide/show controls + +## Installation + +
+Linux + +1. Open a terminal. +2. Go to your Spicetify themes directory: + ```bash + cd ~/.config/spicetify/Themes + ``` +3. Clone or pull this repo into that folder: + ```bash + git clone SpoTUI + ``` + If you already have it locally, update it instead: + ```bash + cd SpoTUI + git pull + ``` +4. Set SpoTUI as your current theme: + ```bash + spicetify config current_theme SpoTUI + ``` +5. Apply Spicetify: + ```bash + spicetify apply + ``` + +
+ +
+Windows + +1. Open PowerShell or Command Prompt. +2. Go to your Spicetify themes directory: + ```powershell + cd $env:APPDATA\spicetify\Themes + ``` +3. Clone or pull this repo into that folder: + ```powershell + git clone SpoTUI + ``` + If you already have it locally, update it instead: + ```powershell + cd SpoTUI + git pull + ``` +4. Set SpoTUI as your current theme: + ```powershell + spicetify config current_theme SpoTUI + ``` +5. Apply Spicetify: + ```powershell + spicetify apply + ``` + +
+ + +## Usage + +After applying the theme, open Spotify and use the SpoTUI interface directly from the client. + +The built-in command list includes: + +- `tui -m [command|cli]` +- `playlist` +- `list` +- `queue` +- `play` +- `pause` +- `p` +- `skip` +- `back` +- `v ` +- `volume ` +- `shuffle` +- `loop [on|off]` +- `superloop [on|off]` +- `search` +- `clear` + +## Theme Files + +- `theme.js` - JavaScript UI logic +- `user.css` - Spotify UI styling overrides +- `color.ini` - theme colors +- `preview.png` - Marketplace preview image + +## Author + +SkenS - https://github.com/SkenSMasteR diff --git a/color.ini b/color.ini new file mode 100644 index 0000000..989b9ae --- /dev/null +++ b/color.ini @@ -0,0 +1,14 @@ +[SpoTUI - SkenS] +text = e6e6e6 +subtext = b3b3b3 +sidebar-text = e6e6e6 +sidebar-background = 0a0a0a +main = 0a0a0a +button = 1a1a1a +button-active = ff8c42 +button-disabled = 2a2a2a +tab-active = ff8c42 +notification = 1a1a1a +notification-error = ff5555 +playback-bar = 2a2a2a +misc = ff8c42 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..0df0cea --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "SpoTUI", + "description": "Terminal interface for Spotify...", + "preview": "preview.png", + "readme": "README.md", + "branch": "release", + "authors": [ + { "name": "SkenS", "url": "https://github.com/SkenSMasteR" } + ], + "tags": ["tui", "beta", "dev", "commands", "terminal", "cli"] +} \ No newline at end of file diff --git a/preview.png b/preview.png new file mode 100644 index 0000000..35b7b25 Binary files /dev/null and b/preview.png differ diff --git a/theme.js b/theme.js new file mode 100644 index 0000000..18060ca --- /dev/null +++ b/theme.js @@ -0,0 +1,1448 @@ +(function () { + +const style = ` +#spotui-tui { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 90px; + width: 100vw; + background: #000; + color: #ddd; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 15px; + padding: 40px; + box-sizing: border-box; + z-index: 9999; + display: flex; + flex-direction: column; + overflow-x: hidden; + user-select: text; + cursor: text; +} + +#spotui-visualizer { + position: absolute; + inset: 0; + display: none; + overflow: hidden; + pointer-events: none; + z-index: 2; + background: + radial-gradient(circle at 50% 65%, rgba(255, 140, 0, 0.12), rgba(0, 0, 0, 0) 42%), + linear-gradient(180deg, rgba(255, 69, 0, 0.08), rgba(0, 0, 0, 0)); +} + +#spotui-visualizer::before { + content: ""; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(255, 140, 0, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 140, 0, 0.03) 1px, transparent 1px); + background-size: 100% 28px, 28px 100%; + opacity: 0.28; +} + +#spotui-visualizer-fall { + position: absolute; + inset: 0 0 90px 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0; + color: #ff8c00; + white-space: pre; + text-align: center; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 24px; + line-height: 1; + opacity: 0.92; + letter-spacing: 0; +} + +.spotui-viz-line { + display: flex; + justify-content: center; + will-change: transform, opacity, filter; + text-shadow: 0 0 12px rgba(255, 140, 0, 0.28); +} + +.spotui-viz-char { + display: inline-block; + will-change: transform, opacity, filter; +} + +#spotui-visualizer-bars { + position: absolute; + left: 50%; + bottom: 114px; + transform: translateX(-50%); + width: min(92vw, 1280px); + height: 34vh; + display: flex; + align-items: flex-end; + justify-content: center; + gap: 6px; + opacity: 0; + transition: opacity 220ms ease; +} + +.spotui-viz-bar { + flex: 1 1 0; + min-width: 4px; + max-width: 14px; + border-radius: 999px 999px 0 0; + background: linear-gradient(180deg, #ffa500 0%, #ff8c00 55%, #ff4500 100%); + box-shadow: + 0 0 12px rgba(255, 140, 0, 0.36), + inset 0 0 0 1px rgba(255, 220, 160, 0.1); + transform-origin: bottom; + min-height: 8px; +} + +#spotui-tui.spotui-has-visualizer #spotui-visualizer { + display: block; +} + +#spotui-logo { + position: absolute; + left: 50%; + top: 41%; + transform: translate(-50%, -50%); + color: #ff8c42; + opacity: 0.28; + white-space: pre; + text-align: center; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 28px; + line-height: 1.0; + pointer-events: none; + user-select: none; + z-index: 0; +} + +#spotui-output { + flex: 1 1 auto; + display: flex; + flex-direction: column-reverse; + white-space: pre-wrap; + line-height: 1.6; + user-select: text; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + position: relative; + z-index: 1; + transition: opacity 260ms ease, transform 260ms ease; +} + +body.spotui-command-mode #spotui-output { + display: none !important; +} + +body.spotui-cli-mode #spotui-output { + display: flex !important; +} + +#spotui-output::-webkit-scrollbar { + width: 0; + height: 0; +} + +#spotui-footer { + display: flex; + align-items: center; + gap: 12px; + padding-top: 12px; + margin-top: auto; + border-top: 1px solid rgba(255, 140, 66, 0.18); + position: relative; + z-index: 1; + transition: opacity 260ms ease, transform 260ms ease; +} + +#spotui-input { + background: transparent; + border: none; + outline: none; + color: #ff8c42; + font-family: inherit; + font-size: inherit; + flex: 1 1 auto; + min-width: 0; +} + +.prompt { color: #ff8c42; } +.cl-line, .result { margin-bottom: 8px; user-select: text; } +.result { padding: 5px; } +.selected { background: #ff8c42; color: #000; } + +#spotui-controls { + display: flex; + gap: 8px; + align-items: center; + margin-left: auto; +} + +.spotui-control-btn { + background: #ff8c42; + color: #000; + border: none; + padding: 6px 12px; + font-family: "JetBrains Mono", monospace; + font-size: 13px; + cursor: pointer; + border-radius: 4px; +} + +.spotui-control-btn:hover { + background: #e07b39; +} + +body.spotui-tui-hidden #spotui-tui { + display: none !important; +} + +body.spotui-tui-hidden #spotui-popup { + display: none !important; +} + +body.spotui-visualizer-active #spotui-output { + opacity: 0; + transform: translateY(0); +} + +body.spotui-visualizer-active #spotui-footer { + opacity: 0; + transform: translateY(0); +} + +body.spotui-visualizer-active #spotui-logo { + opacity: 0; +} + +body.spotui-visualizer-active #spotui-controls { + opacity: 0; +} + +body.spotui-visualizer-active #spotui-visualizer-bars { + opacity: 1; +} + +body.spotui-visualizer-active #spotui-input { + color: rgba(255, 140, 66, 0.58); +} + +#spotui-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: min(480px, calc(100vw - 32px)); + max-height: 60vh; + background: #0a0a0a; + border: 2px solid #ff8c42; + border-radius: 6px; + color: #ddd; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 14px; + z-index: 10002; + padding: 16px; + overflow-y: auto; + outline: none; + box-shadow: 0 0 24px rgba(0,0,0,0.6); + scrollbar-width: none; + -ms-overflow-style: none; +} +#spotui-popup::-webkit-scrollbar { + width: 0; + height: 0; +} +.popup-title { + color: #ff8c42; + margin-bottom: 10px; + font-size: 12px; + border-bottom: 1px solid #333; + padding-bottom: 8px; +} +.popup-row { + padding: 4px 6px; +} +.popup-row.selected { + background: #ff8c42; + color: #000; +} +.popup-input { + width: 100%; + background: #111; + border: 1px solid #ff8c42; + color: #ff8c42; + font-family: inherit; + font-size: inherit; + padding: 6px; + outline: none; + box-sizing: border-box; +} +`; + +const SPOTUI_ASCII_ART = [ + " ██████ ██▓███ ▒█████ ▄▄▄█████▓ █ ██ ██▓", + "▒██ ▒ ▓██░ ██▒▒██▒ ██▒▓ ██▒ ▓▒ ██ ▓██▒▓██▒", + "░ ▓██▄ ▓██░ ██▓▒▒██░ ██▒▒ ▓██░ ▒░▓██ ▒██░▒██▒", + " ▒ ██▒▒██▄█▓▒ ▒▒██ ██░░ ▓██▓ ░ ▓▓█ ░██░░██░", + "▒██████▒▒▒██▒ ░ ░░ ████▓▒░ ▒██▒ ░ ▒▒█████▓ ░██░", + "▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░░ ▒░▒░▒░ ▒ ░░ ░▒▓▒ ▒ ▒ ░▓ ", + "░ ░▒ ░ ░░▒ ░ ░ ▒ ▒░ ░ ░░▒░ ░ ░ ▒ ░", + "░ ░ ░ ░░ ░ ░ ░ ▒ ░ ░░░ ░ ░ ▒ ░", + " ░ ░ ░ ░ ░ ", + " ", +]; + + + +let idleTimer = null; +let animeReady = null; +let visualizerRoot = null; +let visualizerFall = null; +let visualizerBars = null; +let visualizerBarEls = []; +let visualizerBarFrame = null; +let visualizerFallAnim = null; +let visualizerLineAnims = []; +let visualizerBarsTimer = null; +let visualizerActive = false; +let visualizerRequested = false; +let playbackWatcher = null; +let playbackIsPlaying = false; +let tuiMode = "command"; +let visualizerAudioContext = null; +let visualizerAudioSource = null; +let visualizerAnalyser = null; +let visualizerAudioData = null; +let visualizerAudioElement = null; + +let results = []; +let selected = 0; +let lyricsObserver = null; + +function injectStyle() { + const s = document.createElement("style"); + s.textContent = style; + document.head.appendChild(s); +} + +function setTuiMode(mode) { + tuiMode = mode === "cli" ? "cli" : "command"; + document.body.classList.toggle("spotui-cli-mode", tuiMode === "cli"); + document.body.classList.toggle("spotui-command-mode", tuiMode !== "cli"); +} + +function loadAnime() { + if (window.anime) return Promise.resolve(window.anime); + if (animeReady) return animeReady; + + animeReady = new Promise((resolve, reject) => { + const existing = document.querySelector('script[data-spotui-anime="1"]'); + if (existing) { + existing.addEventListener("load", () => resolve(window.anime), { once: true }); + existing.addEventListener("error", () => reject(new Error("anime.js failed to load")), { once: true }); + return; + } + + const script = document.createElement("script"); + script.src = VISUALIZER_ANIME_SRC; + script.async = true; + script.dataset.spotuiAnime = "1"; + script.onload = () => resolve(window.anime); + script.onerror = () => reject(new Error("anime.js failed to load")); + document.head.appendChild(script); + }); + + return animeReady; +} + +function stopVisualizerAnimations() { + try { + visualizerFallAnim?.pause?.(); + visualizerLineAnims.forEach((anim) => anim?.pause?.()); + } catch { + } + + visualizerFallAnim = null; + visualizerLineAnims = []; + if (visualizerBarsTimer) { + clearTimeout(visualizerBarsTimer); + visualizerBarsTimer = null; + } + if (visualizerBarFrame) { + cancelAnimationFrame(visualizerBarFrame); + visualizerBarFrame = null; + } + if (visualizerAudioContext?.state === "running") { + visualizerAudioContext.suspend().catch(() => {}); + } +} + +function ensureVisualizer() { + if (visualizerRoot) return; + + visualizerRoot = document.createElement("div"); + visualizerRoot.id = "spotui-visualizer"; + + visualizerFall = document.createElement("div"); + visualizerFall.id = "spotui-visualizer-fall"; + + visualizerBars = document.createElement("div"); + visualizerBars.id = "spotui-visualizer-bars"; + + visualizerBarEls = []; + for (let i = 0; i < VISUALIZER_BAR_COUNT; i += 1) { + const bar = document.createElement("div"); + bar.className = "spotui-viz-bar"; + bar.style.height = `${8 + (i % 6)}%`; + visualizerBars.appendChild(bar); + visualizerBarEls.push(bar); + } + + visualizerRoot.appendChild(visualizerFall); + visualizerRoot.appendChild(visualizerBars); + document.body.appendChild(visualizerRoot); +} + +function buildVisualizerText() { + visualizerFall.innerHTML = ""; + SPOTUI_ASCII_ART.forEach((line, index) => { + const el = document.createElement("div"); + el.className = "spotui-viz-line"; + for (const ch of line) { + const span = document.createElement("span"); + span.className = "spotui-viz-char"; + span.textContent = ch === " " ? "\u00A0" : ch; + span.dataset.char = ch; + el.appendChild(span); + } + el.dataset.index = String(index); + visualizerFall.appendChild(el); + }); + return Array.from(visualizerFall.children); +} + +function animateVisualizerBars() { + if (visualizerBarFrame) cancelAnimationFrame(visualizerBarFrame); + visualizerBars.style.opacity = "1"; + + const seeds = visualizerBarEls.map((_, index) => ({ + phase: Math.random() * Math.PI * 2, + speed: 0.0012 + (index % 7) * 0.00012, + swing: 0.38 + (index % 5) * 0.08, + })); + + const tryAttachAudio = () => { + const AudioCtor = window.AudioContext || window.webkitAudioContext; + const audioEl = document.querySelector("audio"); + if (!AudioCtor || !audioEl) return false; + + try { + if (visualizerAudioElement !== audioEl) { + if (visualizerAudioSource) { + try { + visualizerAudioSource.disconnect(); + } catch { + } + } + visualizerAudioContext ||= new AudioCtor(); + visualizerAudioSource = visualizerAudioContext.createMediaElementSource(audioEl); + visualizerAnalyser = visualizerAudioContext.createAnalyser(); + visualizerAnalyser.fftSize = 128; + visualizerAnalyser.smoothingTimeConstant = 0.78; + visualizerAudioData = new Uint8Array(visualizerAnalyser.frequencyBinCount); + visualizerAudioSource.connect(visualizerAnalyser); + visualizerAnalyser.connect(visualizerAudioContext.destination); + visualizerAudioElement = audioEl; + } + + if (visualizerAudioContext.state === "suspended") { + visualizerAudioContext.resume().catch(() => {}); + } + return Boolean(visualizerAnalyser); + } catch { + visualizerAnalyser = null; + visualizerAudioData = null; + return false; + } + }; + + const tick = () => { + if (!visualizerActive || !isPlaybackRunning()) { + visualizerBarFrame = null; + return; + } + + const now = performance.now(); + const progress = Number(Spicetify?.Player?.getProgress?.() ?? 0); + const volume = Number(Spicetify?.Player?.getVolume?.() ?? 0.72); + const energyBase = 0.55 + Math.min(0.35, volume * 0.25); + const hasAudio = tryAttachAudio(); + + if (hasAudio && visualizerAnalyser && visualizerAudioData) { + visualizerAnalyser.getByteFrequencyData(visualizerAudioData); + const bucketSize = Math.max(1, Math.floor(visualizerAudioData.length / visualizerBarEls.length)); + visualizerBarEls.forEach((bar, index) => { + let sum = 0; + let peak = 0; + const start = index * bucketSize; + const end = Math.min(visualizerAudioData.length, start + bucketSize); + for (let i = start; i < end; i += 1) { + const value = visualizerAudioData[i] || 0; + sum += value; + if (value > peak) peak = value; + } + const avg = sum / Math.max(1, end - start); + const height = 10 + (avg * 0.72 + peak * 0.28) * 0.32; + bar.style.height = `${Math.max(8, Math.min(100, height))}%`; + }); + } else { + visualizerBarEls.forEach((bar, index) => { + const seed = seeds[index]; + const pulse = Math.sin(now * seed.speed + seed.phase) * 0.5 + 0.5; + const trackWave = Math.sin(progress / 140 + index * 0.42 + seed.phase * 0.7) * 0.5 + 0.5; + const jitter = (Math.sin(now * 0.0025 + index * 0.18) * 0.5 + 0.5) * 0.18; + const height = 10 + (pulse * 0.48 + trackWave * 0.34 + jitter) * (56 + (index % 8) * 2) * energyBase * seed.swing; + bar.style.height = `${Math.max(8, Math.min(100, height))}%`; + }); + } + + visualizerBarFrame = requestAnimationFrame(tick); + }; + + visualizerBarFrame = requestAnimationFrame(tick); +} + +async function enterVisualizerMode() { + if (visualizerActive || visualizerRequested || popup || document.body.classList.contains("spotui-search-mode") || !isPlaybackRunning()) { + return; + } + + visualizerRequested = true; + ensureVisualizer(); + visualizerActive = true; + document.body.classList.add("spotui-visualizer-active"); + document.getElementById("spotui-tui")?.classList.add("spotui-has-visualizer"); + visualizerBars.style.opacity = "0"; + + const lineEls = buildVisualizerText(); + const charEls = lineEls.flatMap((line) => Array.from(line.children)); + + try { + const anime = await loadAnime(); + if (!visualizerActive) { + visualizerRequested = false; + return; + } + + stopVisualizerAnimations(); + + visualizerLineAnims = charEls.map((charEl, index) => { + const lineIndex = Number(charEl.parentElement?.dataset.index || 0); + const columnIndex = Array.from(charEl.parentElement?.children || []).indexOf(charEl); + const dropDistance = 240 + lineIndex * 22 + Math.random() * 160; + const spread = (columnIndex - 24) * 2.1 + (Math.random() - 0.5) * 34; + const drift = (Math.random() - 0.5) * 10; + return anime({ + targets: charEl, + translateY: [0, dropDistance], + translateX: [0, spread + drift], + rotate: [0, (Math.random() - 0.5) * 58], + scale: [1, 0.9], + opacity: [0.92, 0], + duration: 1200 + lineIndex * 140 + (index % 6) * 45, + delay: lineIndex * 130 + columnIndex * 20, + easing: "easeInCubic", + }); + }); + + visualizerFallAnim = anime({ + targets: visualizerFall, + opacity: [0, 1], + duration: 240, + easing: "linear", + }); + + visualizerBarsTimer = setTimeout(() => { + if (!visualizerActive || !isPlaybackRunning()) return; + visualizerBars.style.opacity = "1"; + animateVisualizerBars(); + }, 2100); + } catch { + visualizerBars.style.opacity = "1"; + visualizerBarEls.forEach((bar, index) => { + bar.style.height = `${24 + (index % 7) * 8}%`; + }); + } finally { + visualizerRequested = false; + } +} + +function exitVisualizerMode() { + if (!visualizerActive && !visualizerRequested) return; + visualizerActive = false; + visualizerRequested = false; + document.body.classList.remove("spotui-visualizer-active"); + document.getElementById("spotui-tui")?.classList.remove("spotui-has-visualizer"); + stopVisualizerAnimations(); + if (visualizerBars) visualizerBars.style.opacity = "0"; + if (visualizerRoot) visualizerRoot.replaceChildren(visualizerFall, visualizerBars); +} + +function clearIdleTimer() { + if (idleTimer) { + clearTimeout(idleTimer); + idleTimer = null; + } +} + +function isPlaybackRunning() { + return Boolean(Spicetify?.Player?.isPlaying?.()); +} + +function scheduleIdleTimer() { + clearIdleTimer(); +} + +function noteActivity() { + clearIdleTimer(); +} + +function createCopyButton() { + const controls = document.createElement("div"); + controls.id = "spotui-controls"; + + const copyBtn = document.createElement("button"); + copyBtn.id = "copy-log-btn"; + copyBtn.className = "spotui-control-btn"; + copyBtn.textContent = "Copy log"; + copyBtn.addEventListener("click", () => { + const output = document.getElementById("spotui-output"); + if (output) { + const text = output.innerText; + navigator.clipboard.writeText(text).then(() => { + copyBtn.textContent = "Copied!"; + setTimeout(() => { copyBtn.textContent = "Copy log"; }, 1500); + }).catch(() => { + alert("Copy failed. Please manually select and copy."); + }); + } + }); + + const hideBtn = document.createElement("button"); + hideBtn.id = "hide-tui-btn"; + hideBtn.className = "spotui-control-btn"; + hideBtn.textContent = "Hide TUI"; + hideBtn.addEventListener("click", () => { + const hidden = document.body.classList.toggle("spotui-tui-hidden"); + hideBtn.textContent = hidden ? "Show TUI" : "Hide TUI"; + }); + + const spotifyBtn = document.createElement("button"); + spotifyBtn.id = "enable-spotify-btn"; + spotifyBtn.className = "spotui-control-btn"; + spotifyBtn.textContent = "Enable Spotify"; + spotifyBtn.addEventListener("click", () => { + const enabled = document.body.classList.toggle("spotui-spotify-enabled"); + if (enabled) { + document.body.classList.add("spotui-tui-hidden"); + hideBtn.textContent = "Show TUI"; + spotifyBtn.textContent = "Disable Spotify"; + } else { + spotifyBtn.textContent = "Enable Spotify"; + document.body.classList.remove("spotui-tui-hidden"); + document.body.classList.remove("spotui-search-mode"); + hideBtn.textContent = "Hide TUI"; + noteActivity(); + } + }); + + controls.appendChild(copyBtn); + controls.appendChild(hideBtn); + controls.appendChild(spotifyBtn); + const footer = document.getElementById("spotui-footer"); + (footer || document.body).appendChild(controls); + + const backBtn = document.createElement("button"); + backBtn.id = "spotui-back-btn"; + backBtn.className = "spotui-control-btn"; + backBtn.textContent = "Back"; + backBtn.addEventListener("click", () => { + document.body.classList.remove("spotui-search-mode"); + document.body.classList.remove("spotui-spotify-enabled"); + document.body.classList.remove("spotui-tui-hidden"); + spotifyBtn.textContent = "Enable Spotify"; + hideBtn.textContent = "Hide TUI"; + syncLyricsState(); + noteActivity(); + }); + document.body.appendChild(backBtn); +} + +function detectLyricsSurface() { + return Boolean( + document.querySelector( + ".main-nowPlayingView-lyricsContent, .main-lyricsCinema-container, .lyrics-lyricsContainer-LyricsContainer" + ) + ); +} + +function syncLyricsState() { + if (!document.body) return; + document.body.classList.toggle("spotui-lyrics-open", detectLyricsSurface()); +} + +function hookLyricsButton() { + const button = document.querySelector(".main-nowPlayingBar-lyricsButton"); + if (!button || button.dataset.spotuiTuiLyricsHooked === "1") return; + button.dataset.spotuiTuiLyricsHooked = "1"; + button.addEventListener( + "click", + () => { + setTimeout(syncLyricsState, 50); + setTimeout(syncLyricsState, 250); + setTimeout(syncLyricsState, 1000); + }, + true + ); +} + +function initLyricsBridge() { + if (!document.body) { + setTimeout(initLyricsBridge, 250); + return; + } + + const refresh = () => { + hookLyricsButton(); + syncLyricsState(); + }; + + refresh(); + + if (!lyricsObserver) { + lyricsObserver = new MutationObserver(refresh); + lyricsObserver.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["class", "style"], + }); + window.addEventListener( + "beforeunload", + () => { + lyricsObserver?.disconnect(); + }, + { once: true } + ); + } +} + +function createTerminal() { + const box = document.createElement("div"); + box.id = "spotui-tui"; + setTuiMode("command"); + box.innerHTML = ` + +
SpoTUI v1.0 + +Type /help + +
+ +`; + document.body.appendChild(box); + + const input = document.getElementById("spotui-input"); + input.addEventListener("input", noteActivity); + input.addEventListener("keydown", async (e) => { + noteActivity(); + if (e.key === "Enter") { + const cmd = input.value.trim(); + input.value = ""; + print("> " + cmd); + await execute(cmd); + } + if (e.key === "ArrowDown" && results.length) { + selected = Math.min(selected + 1, results.length - 1); + renderResults(); + } + if (e.key === "ArrowUp" && results.length) { + selected = Math.max(selected - 1, 0); + renderResults(); + } + }); + + document.addEventListener("pointerdown", noteActivity, true); +} + +function print(text) { + if (tuiMode !== "cli") return; + const output = document.getElementById("spotui-output"); + const line = document.createElement("div"); + line.className = "cl-line"; + line.textContent = text; + output.prepend(line); + output.scrollTop = 0; +} + +async function execute(cmd) { + const rawCmd = cmd.trim(); + const cleanedCmd = rawCmd.startsWith("/") || rawCmd.startsWith(".") ? rawCmd.slice(1).trim() : rawCmd; + const [command, ...args] = cleanedCmd.split(/\s+/); + const argText = args.join(" ").trim(); + + if (command === "tui") { + if (args[0] === "-m") { + const mode = args[1]; + if (!mode || (mode !== "cli" && mode !== "command")) { + print("Usage: tui -m [command|cli]"); + return; + } + setTuiMode(mode); + print(`TUI mode: ${mode}`); + return; + } + print("Usage: tui -m [command|cli]"); + return; + } + + if (command === "help") { + popupMode = "help"; + renderPopup(); + return; + } + if (command === "clear") { + document.getElementById("spotui-output").textContent = ""; + noteActivity(); + return; + } + + if (command === "playlist") { + await openPlaylistPopup(); + return; + } + + if (command === "list") { + await openPlaylistTracksPopup(); + return; + } + + if (command === "queue") { + await openQueuePopup(); + return; + } + + if (command === "play") { + try { + const wasPlaying = Spicetify.Player.isPlaying(); + if (!wasPlaying) { + Spicetify.Player.togglePlay(); + } + print("Playing"); + } catch (err) { + print("Play error: " + err.message); + } + return; + } + + if (command === "pause") { + try { + const wasPlaying = Spicetify.Player.isPlaying(); + if (wasPlaying) { + Spicetify.Player.togglePlay(); + } + print("Paused"); + } catch (err) { + print("Pause error: " + err.message); + } + return; + } + + if (command === "p") { + try { + const wasPlaying = Spicetify.Player.isPlaying(); + Spicetify.Player.togglePlay(); + print(wasPlaying ? "Paused" : "Playing"); + } catch (err) { + print("Play/pause error: " + err.message); + } + return; + } + + if (command === "search") { + document.body.classList.add("spotui-search-mode"); + document.body.classList.add("spotui-tui-hidden"); + syncLyricsState(); + clearIdleTimer(); + return; + } + + if (command === "skip") { + try { + Spicetify.Player.next(); + print("Skipped to next track"); + } catch (err) { + print("Skip error: " + err.message); + } + return; + } + if (command === "back") { + try { + Spicetify.Player.back(); + print("Went back to previous track"); + } catch (err) { + print("Back error: " + err.message); + } + return; + } + + if (command === "volume" || command === "v") { + try { + if (!argText) { + print("Usage: volume <0-100>"); + return; + } + const percent = Number(argText); + if (!Number.isFinite(percent) || percent < 0 || percent > 100) { + print("Volume must be between 0 and 100."); + return; + } + Spicetify.Player.setVolume(percent / 100); + print(`Volume is ${Math.round(percent)}%`); + } catch (err) { + print("Volume error: " + err.message); + } + return; + } + + if (command === "shuffle") { + try { + const current = Spicetify.Player.getShuffle(); + Spicetify.Player.setShuffle(!current); + print("Shuffle: " + (!current ? "ON" : "OFF")); + } catch (err) { + print("Shuffle error: " + err.message); + } + return; + } + + if (command === "loop") { + handleRepeatCommand("loop", argText); + return; + } + + if (command === "superloop") { + handleRepeatCommand("superloop", argText); + return; + } + + print("Unknown command. Type /help"); +} + +function renderResults() { + const output = document.getElementById("spotui-output"); + output.textContent = ""; + results.forEach((item, idx) => { + const line = document.createElement("div"); + line.className = "result" + (idx === selected ? " selected" : ""); + line.textContent = `${idx + 1}. ${item.name}${item.artist ? " - " + item.artist : ""}`; + output.appendChild(line); + }); +} + +let popup = null; +let popupMode = "list"; +let playlists = []; +let popupSelected = 0; +let popupTarget = null; +let popupTracks = []; +let popupTrackSelected = 0; +let popupTrackTitle = ""; +let popupTrackContext = null; +let lastPlaylistContextUri = null; +let lastQueueSnapshot = null; + +function getTrackTitle(track, index = 0) { + const meta = track?.metadata || track?.contextTrack?.metadata || {}; + return track?.name || track?.title || meta.title || meta.name || `Track ${index + 1}`; +} + +function getTrackArtist(track) { + const meta = track?.metadata || track?.contextTrack?.metadata || {}; + if (track?.artist) return track.artist; + if (Array.isArray(track?.artists) && track.artists.length) { + return track.artists.map((artist) => artist?.name).filter(Boolean).join(", "); + } + if (meta.artist_name) return meta.artist_name; + if (meta["artist_name:1"]) return meta["artist_name:1"]; + return ""; +} + +function normalizeTrackItem(track, index = 0) { + const uri = track?.uri || track?.contextTrack?.uri || ""; + return { + uri, + name: getTrackTitle(track, index), + artist: getTrackArtist(track), + }; +} + +function dedupeTracks(tracks) { + const seen = new Set(); + const out = []; + for (const track of tracks) { + if (!track?.uri || seen.has(track.uri)) continue; + seen.add(track.uri); + out.push(track); + } + return out; +} + +function findTrackIndexByUri(tracks, uri) { + if (!uri) return -1; + return tracks.findIndex((track) => track?.uri === uri); +} + +function getCurrentPlaylistContextUri() { + const current = Spicetify.Player.data?.context?.uri; + if (current && Spicetify.URI.isPlaylistV1OrV2(current)) return current; + return lastPlaylistContextUri; +} + +function getPlaylistId(uri) { + try { + return Spicetify.URI.fromString(uri)?.id ?? uri.split(":").pop(); + } catch { + return uri.split(":").pop(); + } +} + +function handleRepeatCommand(kind, arg) { + try { + const current = Spicetify.Player.getRepeat(); + const targetMode = kind === "loop" ? 1 : 2; + let nextMode = targetMode; + + if (arg === "on") { + nextMode = targetMode; + } else if (arg === "off") { + nextMode = 0; + } else if (arg === "") { + nextMode = current === targetMode ? 0 : targetMode; + } else { + print(`Usage: /${kind} [on|off]`); + return; + } + + Spicetify.Player.setRepeat(nextMode); + print(`${kind === "loop" ? "Loop" : "Superloop"}: ${nextMode === 0 ? "OFF" : "ON"}`); + } catch (err) { + print(`${kind === "loop" ? "Loop" : "Superloop"} error: ${err.message}`); + } +} + +async function getPlaylists() { + const rootlist = await Spicetify.Platform.RootlistAPI.getContents(); + const list = []; + function flatten(items) { + for (const item of items) { + if (item.type === "playlist") { + list.push({ name: item.name, uri: item.uri }); + } else if (item.type === "folder" && item.items) { + flatten(item.items); + } + } + } + flatten(rootlist.items); + return list; +} + +async function openPlaylistPopup() { + clearIdleTimer(); + try { + playlists = await getPlaylists(); + } catch (err) { + print("Playlist error: " + err.message); + scheduleIdleTimer(); + return; + } + popupSelected = 0; + popupMode = "list"; + renderPopup(); +} + +async function openPlaylistTracksPopup() { + clearIdleTimer(); + try { + const contextUri = getCurrentPlaylistContextUri(); + if (!contextUri || !Spicetify.URI.isPlaylistV1OrV2(contextUri)) { + print("List error: not currently playing a playlist."); + scheduleIdleTimer(); + return; + } + const res = await Spicetify.Platform.PlaylistAPI.getContents(contextUri, { + limit: 9999999, + }); + popupTracks = (res.items || []) + .filter((item) => item && item.uri && item.isPlayable !== false) + .map((item, index) => normalizeTrackItem(item, index)); + popupTracks = dedupeTracks(popupTracks); + popupTrackTitle = "Playlist tracks"; + popupTrackContext = Spicetify.Player.data?.context || { uri: contextUri }; + lastPlaylistContextUri = contextUri; + const currentTrackUri = Spicetify.Player.data?.item?.uri; + popupTrackSelected = Math.max(findTrackIndexByUri(popupTracks, currentTrackUri), 0); + popupMode = "tracks"; + renderPopup(); + } catch (err) { + print("List error: " + err.message); + console.error("Playlist tracks popup error:", err); + scheduleIdleTimer(); + } +} + +function getQueueTracks() { + const queue = Spicetify.Queue || {}; + const tracks = []; + + if (queue.track && queue.track.uri) { + tracks.push(queue.track); + } else if (Spicetify.Player.data?.item?.uri) { + tracks.push(Spicetify.Player.data.item); + } + + for (const item of queue.nextTracks || []) { + const track = item?.contextTrack || item?.track || item; + if (!track?.uri || track.uri === "spotify:delimiter") continue; + tracks.push(track); + } + + const currentTrackContextUri = + Spicetify.Player.data?.context_uri || + Spicetify.Player.data?.context?.uri || + queue.track?.contextTrack?.metadata?.context_uri || + queue.track?.metadata?.context_uri || + null; + + popupTrackContext = currentTrackContextUri ? { uri: currentTrackContextUri } : Spicetify.Player.data?.context || null; + + return dedupeTracks(tracks.map((track, index) => normalizeTrackItem(track, index))); +} + +function openQueuePopup() { + clearIdleTimer(); + const currentQueueTracks = getQueueTracks(); + if (!lastQueueSnapshot || lastQueueSnapshot.length === 0) { + lastQueueSnapshot = currentQueueTracks; + } + popupTracks = lastQueueSnapshot.length ? lastQueueSnapshot : currentQueueTracks; + popupTrackTitle = "Queue"; + const currentTrackUri = Spicetify.Player.data?.item?.uri; + popupTrackSelected = Math.max(findTrackIndexByUri(popupTracks, currentTrackUri), 0); + popupMode = "tracks"; + renderPopup(); +} + +function closePopup() { + if (popup) { + popup.remove(); + popup = null; + } + popupMode = "list"; + popupTracks = []; + popupTrackSelected = 0; + popupTrackTitle = ""; + popupTrackContext = null; + const input = document.getElementById("spotui-input"); + if (input) input.focus(); + scheduleIdleTimer(); +} + +function renderPopup() { + if (popup) popup.remove(); + popup = document.createElement("div"); + popup.id = "spotui-popup"; + popup.tabIndex = 0; + + if (popupMode === "list") { + popup.innerHTML = ``; + document.body.appendChild(popup); + const listEl = popup.querySelector("#popup-list"); + if (!playlists.length) { + const row = document.createElement("div"); + row.className = "popup-row"; + row.textContent = "No playlists found."; + listEl.appendChild(row); + } else { + playlists.forEach((p, idx) => { + const row = document.createElement("div"); + row.className = "popup-row" + (idx === popupSelected ? " selected" : ""); + row.textContent = `${idx + 1}. ${p.name}`; + listEl.appendChild(row); + }); + } + popup.addEventListener("keydown", handleListKeydown); + popup.focus(); + } else if (popupMode === "new") { + popup.innerHTML = ``; + document.body.appendChild(popup); + const inputEl = popup.querySelector("#popup-input"); + inputEl.focus(); + inputEl.addEventListener("keydown", handleNewKeydown); + } else if (popupMode === "rename") { + const target = playlists[popupTarget]; + popup.innerHTML = ``; + document.body.appendChild(popup); + const inputEl = popup.querySelector("#popup-input"); + inputEl.focus(); + inputEl.select(); + inputEl.addEventListener("keydown", handleRenameKeydown); + } else if (popupMode === "delete-confirm") { + const target = playlists[popupTarget]; + popup.innerHTML = ``; + document.body.appendChild(popup); + popup.addEventListener("keydown", handleDeleteKeydown); + popup.focus(); + } else if (popupMode === "tracks") { + popup.innerHTML = ``; + document.body.appendChild(popup); + const listEl = popup.querySelector("#popup-list"); + if (!popupTracks.length) { + const row = document.createElement("div"); + row.className = "popup-row"; + row.textContent = "No tracks found."; + listEl.appendChild(row); + } else { + popupTracks.forEach((track, idx) => { + const row = document.createElement("div"); + row.className = "popup-row" + (idx === popupTrackSelected ? " selected" : ""); + row.textContent = `${idx + 1}. ${track.name}${track.artist ? " - " + track.artist : ""}`; + listEl.appendChild(row); + }); + } + popup.addEventListener("keydown", handleTrackListKeydown); + popup.focus(); + scrollSelectedPopupRowIntoView(); + } else if (popupMode === "help") { + popup.innerHTML = ``; + document.body.appendChild(popup); + const listEl = popup.querySelector("#popup-list"); + [ + "tui -m [command|cli]", + "playlist", + "list", + "queue", + "play", + "pause", + "p", + "skip", + "back", + "v ", + "volume ", + "shuffle", + "loop [on|off]", + "superloop [on|off]", + "search", + "clear", + ].forEach((item) => { + const row = document.createElement("div"); + row.className = "popup-row"; + row.textContent = item; + listEl.appendChild(row); + }); + popup.addEventListener("keydown", handleHelpKeydown); + popup.focus(); + } +} + +function scrollSelectedPopupRowIntoView() { + requestAnimationFrame(() => { + const selectedRow = popup?.querySelector(".popup-row.selected"); + if (selectedRow?.scrollIntoView) { + selectedRow.scrollIntoView({ block: "nearest" }); + } + }); +} + +function handleListKeydown(e) { + if (e.key === "ArrowDown") { + e.preventDefault(); + if (playlists.length) popupSelected = (popupSelected + 1) % playlists.length; + renderPopup(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + if (playlists.length) popupSelected = (popupSelected - 1 + playlists.length) % playlists.length; + renderPopup(); + } else if (e.key === "Enter") { + e.preventDefault(); + const p = playlists[popupSelected]; + if (p) { + lastPlaylistContextUri = p.uri; + Spicetify.Player.playUri(p.uri); + print("Playing playlist: " + p.name); + } + closePopup(); + } else if (e.key === "n" || e.key === "N") { + e.preventDefault(); + popupMode = "new"; + renderPopup(); + } else if (e.key === "r" || e.key === "R") { + e.preventDefault(); + if (playlists.length) { + popupTarget = popupSelected; + popupMode = "rename"; + renderPopup(); + } + } else if (e.key === "d" || e.key === "D") { + e.preventDefault(); + if (playlists.length) { + popupTarget = popupSelected; + popupMode = "delete-confirm"; + renderPopup(); + } + } else if (e.key === "Escape") { + e.preventDefault(); + closePopup(); + } + scrollSelectedPopupRowIntoView(); +} + +function handleTrackListKeydown(e) { + if (e.key === "ArrowDown") { + e.preventDefault(); + if (popupTracks.length) popupTrackSelected = (popupTrackSelected + 1) % popupTracks.length; + renderPopup(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + if (popupTracks.length) popupTrackSelected = (popupTrackSelected - 1 + popupTracks.length) % popupTracks.length; + renderPopup(); + } else if (e.key === "Enter") { + e.preventDefault(); + const track = popupTracks[popupTrackSelected]; + if (track?.uri) { + Spicetify.Player.playUri(track.uri, popupTrackContext || undefined); + print("Playing: " + track.name); + } + if (popupTrackTitle === "Queue") { + lastQueueSnapshot = popupTracks.slice(); + } + closePopup(); + } else if (e.key === "Escape") { + e.preventDefault(); + closePopup(); + } + scrollSelectedPopupRowIntoView(); +} + +function handleHelpKeydown(e) { + if (e.key === "Escape" || e.key === "Enter") { + e.preventDefault(); + closePopup(); + } +} + +async function handleNewKeydown(e) { + if (e.key === "Enter") { + e.preventDefault(); + const name = e.target.value.trim(); + if (!name) return; + try { + await Spicetify.Platform.RootlistAPI.createPlaylist(name, {}); + print("Created playlist: " + name); + } catch (err) { + print("Create failed: " + err.message + " (check console)"); + console.error("Create playlist error:", err); + } + playlists = await getPlaylists(); + popupSelected = 0; + popupMode = "list"; + renderPopup(); + } else if (e.key === "Escape") { + e.preventDefault(); + popupMode = "list"; + renderPopup(); + } +} + +async function handleRenameKeydown(e) { + if (e.key === "Enter") { + e.preventDefault(); + const name = e.target.value.trim(); + const target = playlists[popupTarget]; + if (!name || !target) return; + try { + const playlistId = getPlaylistId(target.uri); + await Spicetify.CosmosAsync.put(`https://api.spotify.com/v1/playlists/${playlistId}`, { + name, + }); + print("Renamed to: " + name); + } catch (err) { + print("Rename failed: " + err.message + " (check console)"); + console.error("Rename playlist error:", err); + } + playlists = await getPlaylists(); + popupSelected = 0; + popupMode = "list"; + renderPopup(); + } else if (e.key === "Escape") { + e.preventDefault(); + popupMode = "list"; + renderPopup(); + } +} + +async function handleDeleteKeydown(e) { + if (e.key === "y" || e.key === "Y") { + e.preventDefault(); + const target = playlists[popupTarget]; + try { + const playlistId = getPlaylistId(target.uri); + await Spicetify.CosmosAsync.del(`https://api.spotify.com/v1/playlists/${playlistId}/followers`); + print("Deleted playlist: " + target.name); + } catch (err) { + print("Delete failed: " + err.message + " (check console)"); + console.error("Delete playlist error:", err); + } + playlists = await getPlaylists(); + popupSelected = 0; + popupMode = "list"; + renderPopup(); + } else if (e.key === "n" || e.key === "N" || e.key === "Escape") { + e.preventDefault(); + popupMode = "list"; + renderPopup(); + } +} + +injectStyle(); +setTimeout(createCopyButton, 500); +setTimeout(initLyricsBridge, 1000); + +if (Spicetify?.Platform) { + createTerminal(); +} else { + setTimeout(createTerminal, 1500); +} + +})(); diff --git a/user.css b/user.css new file mode 100644 index 0000000..37c2833 --- /dev/null +++ b/user.css @@ -0,0 +1,486 @@ +.Root__main-view, +.Root__nav-bar, +.main-topBar-container, +.main-yourLibraryX-libraryRootlist, +.main-yourLibraryX-entryPoints, +.main-buddy-list, +.main-trackList-trackList, +.main-card-card, +.main-gridContainer-gridContainer, +.main-entityHeader-container { + display: none !important; +} + +body.spotui-spotify-enabled .Root__main-view, +body.spotui-spotify-enabled .Root__nav-bar, +body.spotui-spotify-enabled .main-topBar-container, +body.spotui-spotify-enabled .main-appShell-topBar, +body.spotui-spotify-enabled .main-yourLibraryX-libraryRootlist, +body.spotui-spotify-enabled .main-yourLibraryX-entryPoints, +body.spotui-spotify-enabled .Root__right-sidebar, +body.spotui-spotify-enabled .main-buddy-list, +body.spotui-spotify-enabled .main-nowPlayingView-section, +body.spotui-spotify-enabled .main-trackList-trackList, +body.spotui-spotify-enabled .main-card-card, +body.spotui-spotify-enabled .main-gridContainer-gridContainer, +body.spotui-spotify-enabled .main-entityHeader-container { + display: revert !important; +} + +body.spotui-spotify-enabled .Root__main-view, +body.spotui-spotify-enabled .main-view-container { + display: block !important; +} + +body.spotui-spotify-enabled .Root__nav-bar, +body.spotui-spotify-enabled .main-topBar-container, +body.spotui-spotify-enabled .main-appShell-topBar, +body.spotui-spotify-enabled .main-yourLibraryX-libraryRootlist, +body.spotui-spotify-enabled .main-yourLibraryX-entryPoints, +body.spotui-spotify-enabled .Root__right-sidebar, +body.spotui-spotify-enabled .main-buddy-list, +body.spotui-spotify-enabled .main-nowPlayingView-section, +body.spotui-spotify-enabled .main-trackList-trackList { + display: flex !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar { + background: #000 !important; + border-bottom: 1px solid #ff8c42 !important; + box-shadow: inset 0 -1px 0 rgba(255, 140, 66, 0.18) !important; + color: #ddd !important; + font-family: "JetBrains Mono", "Fira Code", monospace !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar .main-topBar-background, +body.spotui-spotify-enabled .main-appShell-topBar .main-topBar-overlay, +body.spotui-spotify-enabled .main-appShell-topBar .main-topBar-withBackgroundBlur { + background: #000 !important; + background-image: none !important; + opacity: 1 !important; + backdrop-filter: none !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchSection, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchContainer, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputContainer, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputSection, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputWrapper, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputTextWrapper, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-historyButtonsContainer, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-historyButtonsWrapper, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-contentRight { + display: flex !important; + align-items: center !important; + gap: 8px !important; + min-height: 56px !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-navLink, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-historyButtons button, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchContainer button, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchContainer [role="button"] { + color: #ff8c42 !important; + border-radius: 0 !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-navLink:hover, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-historyButtons button:hover, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchContainer button:hover, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchContainer [role="button"]:hover { + color: #ffb26f !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchContainer, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputContainer { + background: #000 !important; + border: 1px solid #ff8c42 !important; + border-radius: 0 !important; + box-shadow: inset 0 1px 0 rgba(255, 140, 66, 0.15) !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputText, +body.spotui-spotify-enabled .main-appShell-topBar .x-searchInput-searchInputInput, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputTextWrapper input { + color: #ff8c42 !important; + background: transparent !important; + border: none !important; + outline: none !important; + font-family: "JetBrains Mono", "Fira Code", monospace !important; + caret-color: #ff8c42 !important; +} + +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputText::placeholder, +body.spotui-spotify-enabled .main-appShell-topBar .x-searchInput-searchInputInput::placeholder, +body.spotui-spotify-enabled .main-appShell-topBar .main-globalNav-searchInputTextWrapper input::placeholder { + color: #777 !important; +} + +.main-connectBar-connectBar, +.main-devicePicker-button, +.main-devicePicker-controlButton, +.main-devicePicker-moreButton, +.main-devicePicker-tooltip { + display: none !important; +} + +.Root__now-playing-bar { + display: flex !important; + position: fixed !important; + bottom: 0; + left: 0; + right: 0; + width: 100vw !important; + height: 90px; + border-radius: 0 !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-container, +.Root__now-playing-bar .main-nowPlayingBar-nowPlayingBar { + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; + flex: 1 1 auto !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-nowPlayingBar { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + background: #000 !important; + border-top: 1px solid #ff8c42; + box-shadow: inset 0 1px 0 rgba(255, 140, 66, 0.18); + color: #ddd !important; + font-family: "JetBrains Mono", "Fira Code", monospace !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-left, +.Root__now-playing-bar .main-nowPlayingBar-center, +.Root__now-playing-bar .main-nowPlayingBar-right { + min-width: 0 !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-left { + flex: 1.2 1 0 !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-center { + flex: 1.8 1 0 !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-right { + flex: 1.1 1 0 !important; + justify-content: flex-end !important; +} + +.Root__now-playing-bar .main-trackInfo-container, +.Root__now-playing-bar .main-trackInfo-contentContainer, +.Root__now-playing-bar .main-trackInfo-contentWrapper { + min-width: 0 !important; +} + +.Root__now-playing-bar .main-trackInfo-name { + color: #ff8c42 !important; + font-family: inherit !important; + letter-spacing: 0.02em; +} + +.Root__now-playing-bar .main-trackInfo-artists a, +.Root__now-playing-bar .main-trackInfo-artists { + color: #b8b8b8 !important; + font-family: inherit !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-container button, +.Root__now-playing-bar .main-nowPlayingBar-container [role="button"], +.Root__now-playing-bar .volume-bar__icon-button { + border-radius: 0 !important; + color: #ff8c42 !important; +} + +.Root__now-playing-bar .main-nowPlayingBar-container button:hover, +.Root__now-playing-bar .main-nowPlayingBar-container [role="button"]:hover, +.Root__now-playing-bar .volume-bar__icon-button:hover { + color: #ffb26f !important; +} + +.Root__now-playing-bar .x-progressBar-progressBarBg, +.Root__now-playing-bar .x-progressBar-progressBar { + background: #202020 !important; +} + +.Root__now-playing-bar .x-progressBar-progressFillColor, +.Root__now-playing-bar .x-progressBar-fillColor, +.Root__now-playing-bar .x-progressBar-handle { + background: #ff8c42 !important; +} + +.Root__now-playing-bar .volume-bar { + color: #ff8c42 !important; +} + +.Root__now-playing-bar .volume-bar__slider-container { + filter: saturate(0.9); +} + +body:not(.spotui-lyrics-open) .Root__right-sidebar, +body:not(.spotui-lyrics-open) .main-nowPlayingView-section, +body:not(.spotui-lyrics-open) .main-lyricsCinema-container, +body:not(.spotui-lyrics-open) .lyrics-lyrics-container, +body:not(.spotui-lyrics-open) .lyrics-lyricsContainer-LyricsContainer { + display: none !important; +} + +body.spotui-lyrics-open .Root__right-sidebar, +body.spotui-lyrics-open .main-nowPlayingView-section, +body.spotui-lyrics-open .main-lyricsCinema-container { + display: flex !important; +} + +body.spotui-lyrics-open .Root__right-sidebar { + width: min(420px, 100vw) !important; + min-width: min(420px, 100vw) !important; + max-width: min(420px, 100vw) !important; +} + +body.spotui-lyrics-open .Root__right-sidebar, +body.spotui-lyrics-open .main-nowPlayingView-section, +body.spotui-lyrics-open .main-lyricsCinema-container, +body.spotui-lyrics-open .lyrics-lyrics-container, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsContainer { + background: #000 !important; + color: #ddd !important; + font-family: "JetBrains Mono", "Fira Code", monospace !important; + border-top: 1px solid #ff8c42 !important; + box-shadow: inset 0 1px 0 rgba(255, 140, 66, 0.18) !important; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsContent, +body.spotui-lyrics-open .main-nowPlayingView-lyricsControls, +body.spotui-lyrics-open .main-lyricsCinema-content, +body.spotui-lyrics-open .lyrics-lyrics-container, +body.spotui-lyrics-open .lyrics-lyrics-contentContainer, +body.spotui-lyrics-open .lyrics-lyrics-contentWrapper, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsContainer, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsBackground { + background: #000 !important; + color: #ddd !important; + padding: 20px 24px 28px !important; + box-sizing: border-box !important; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsContent, +body.spotui-lyrics-open .main-lyricsCinema-container, +body.spotui-lyrics-open .lyrics-lyrics-container, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsContainer { + overflow: auto !important; + scrollbar-width: none !important; + -ms-overflow-style: none !important; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsContent::-webkit-scrollbar, +body.spotui-lyrics-open .main-lyricsCinema-container::-webkit-scrollbar, +body.spotui-lyrics-open .lyrics-lyrics-container::-webkit-scrollbar, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsContainer::-webkit-scrollbar { + width: 0 !important; + height: 0 !important; +} + +body.spotui-lyrics-open .main-nowPlayingBar-lyricsButton { + color: #ff8c42 !important; + border-radius: 0 !important; +} + +body.spotui-lyrics-open .main-nowPlayingBar-lyricsButton:hover { + color: #ffb26f !important; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsTitle, +body.spotui-lyrics-open .lyrics-lyricsContent-provider, +body.spotui-lyrics-open .lyrics-lyricsContainer-Provider { + color: #ff8c42 !important; + font-family: inherit !important; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsContent, +body.spotui-lyrics-open .lyrics-lyricsContent-text, +body.spotui-lyrics-open .lyrics-lyricsContent-lyric, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsLine, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsLine a { + color: #b8b8b8 !important; + font-family: inherit !important; + line-height: 1.55 !important; +} + +body.spotui-lyrics-open .lyrics-lyricsContent-active, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsLine-active, +body.spotui-lyrics-open .lyrics-lyricsContainer-Karaoke-WordActive { + color: #ff8c42 !important; +} + +body.spotui-lyrics-open .lyrics-lyricsContent-previous, +body.spotui-lyrics-open .lyrics-lyricsContent-upcoming, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsLine-past { + color: #777 !important; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsGradient, +body.spotui-lyrics-open .lyrics-lyrics-background, +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsBackground { + background: linear-gradient(180deg, rgba(255, 140, 66, 0.12), rgba(0, 0, 0, 0)) !important; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsControls button, +body.spotui-lyrics-open .main-nowPlayingView-lyricsControls [role="button"], +body.spotui-lyrics-open .lyrics-config-button, +body.spotui-lyrics-open .lyrics-config-button-container button { + border-radius: 0 !important; + color: #ff8c42 !important; +} + +body.spotui-lyrics-open .main-nowPlayingView-lyricsControls button:hover, +body.spotui-lyrics-open .main-nowPlayingView-lyricsControls [role="button"]:hover, +body.spotui-lyrics-open .lyrics-config-button:hover, +body.spotui-lyrics-open .lyrics-config-button-container button:hover { + color: #ffb26f !important; +} + +body.spotui-lyrics-open .lyrics-lyricsContainer-LyricsLine { + line-height: 1.55 !important; +} + +.Root__main-view, +.main-view-container, +.main-buddy-list { + background: #000 !important; +} + +body { + background: #000 !important; + overflow: hidden !important; +} + +body.spotui-spotify-enabled { + overflow: auto !important; +} + +html:has(body.spotui-spotify-enabled) { + overflow: auto !important; + height: 100% !important; +} + +body.spotui-spotify-enabled .Root, +body.spotui-spotify-enabled .Root__main-view, +body.spotui-spotify-enabled .main-view-container { + position: relative !important; + height: calc(100vh - 90px) !important; + overflow: auto !important; +} + +#spotui-tui { + width: 100vw !important; + max-width: 100vw !important; + box-sizing: border-box !important; + overflow-x: hidden !important; +} + +body.spotui-spotify-enabled #spotui-tui { + display: flex !important; +} + +body.spotui-tui-hidden #spotui-tui { + display: none !important; +} + +body.spotui-spotify-enabled .main-topBar-searchBar, +body.spotui-spotify-enabled .search-modal-searchBar, +body.spotui-spotify-enabled .collection-searchBar-searchBar { + background: #000 !important; + border: 1px solid #ff8c42 !important; + border-radius: 0 !important; + box-shadow: inset 0 1px 0 rgba(255, 140, 66, 0.15) !important; +} + +body.spotui-spotify-enabled .main-globalNav-searchInputContainer, +body.spotui-spotify-enabled .main-globalNav-searchInputSection, +body.spotui-spotify-enabled .main-globalNav-searchInputWrapper, +body.spotui-spotify-enabled .search-modal-searchBar { + background: #000 !important; +} + +body.spotui-spotify-enabled .x-searchInput-searchInputInput, +body.spotui-spotify-enabled .main-globalNav-searchInputText, +body.spotui-spotify-enabled .search-modal-searchBar input { + color: #ff8c42 !important; + background: transparent !important; + border: none !important; + outline: none !important; + font-family: "JetBrains Mono", "Fira Code", monospace !important; + caret-color: #ff8c42 !important; +} + +body.spotui-spotify-enabled .x-searchInput-searchInputInput::placeholder, +body.spotui-spotify-enabled .main-globalNav-searchInputText::placeholder, +body.spotui-spotify-enabled .search-modal-searchBar input::placeholder { + color: #777 !important; +} + +body.spotui-spotify-enabled .x-searchInput-searchInputSearchIcon, +body.spotui-spotify-enabled .x-searchInput-searchInputClearButton, +body.spotui-spotify-enabled .x-searchInput-searchInputClearIcon { + color: #ff8c42 !important; +} + +body.spotui-spotify-enabled .x-searchInput-searchInputClearButton:hover, +body.spotui-spotify-enabled .x-searchInput-searchInputSearchIcon:hover { + color: #ffb26f !important; +} + +body.spotui-lyrics-open #spotui-tui { + width: calc(100vw - 420px) !important; + max-width: calc(100vw - 420px) !important; +} + +body.spotui-search-mode #spotui-tui { + display: none !important; +} + +body.spotui-search-mode #spotui-controls { + display: none !important; +} + +body.spotui-tui-hidden #spotui-back-btn, +body.spotui-spotify-enabled #spotui-back-btn, +body.spotui-search-mode #spotui-back-btn { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + z-index: 10020 !important; + display: block !important; + min-width: 160px; + padding: 10px 16px; +} + +body:not(.spotui-tui-hidden):not(.spotui-spotify-enabled):not(.spotui-search-mode) #spotui-back-btn { + display: none !important; +} + +#spotui-popup { + width: min(480px, calc(100vw - 32px)) !important; +} + +.main-topBar-topbarContent { + display: none !important; +} + +body.spotui-spotify-enabled .main-topBar-topbarContent { + display: flex !important; +} + +button, +[role="button"] { + border-radius: 0 !important; +}