feat(theme): Add theme browser /theme

This commit is contained in:
Damjan Antevski
2026-08-07 02:52:20 +02:00
parent 9fc6d89ba8
commit cee15061c0
2 changed files with 156 additions and 12 deletions
+8
View File
@@ -85,8 +85,16 @@ Type `help` in the SpoTUI command bar to see a list of available commands.
| `lyrics` | Toggle lyrics panel | | `lyrics` | Toggle lyrics panel |
| `search` | Open Spotify's native search | | `search` | Open Spotify's native search |
| `clear` | Clear the TUI output | | `clear` | Clear the TUI output |
| `theme` | Browse and apply themes |
| `help` | Show the help panel | | `help` | Show the help panel |
## Contributing
You can add your own theme to the theme browser by visiting [spotui.root.sx](https://spotui.root.sx/).
**Note**: You can only submit one theme every 24 hours.
## Notable forks of my project ## Notable forks of my project
<details> <details>
+148 -12
View File
@@ -1,5 +1,7 @@
(function () { (function () {
const ANIMATION_KEY = "spotui:ascii-animation"; const ANIMATION_KEY = "spotui:ascii-animation";
const THEME_HOST = "https://spotui.root.sx/";
const style = ` const style = `
#spotui-tui { #spotui-tui {
@@ -44,6 +46,7 @@ const style = `
body.spotui-lyrics-panel #spotui-logo, body.spotui-lyrics-panel #spotui-logo,
body.spotui-playlist-panel #spotui-logo, body.spotui-playlist-panel #spotui-logo,
body.spotui-help-panel #spotui-logo, body.spotui-help-panel #spotui-logo,
body.spotui-theme-panel #spotui-logo,
body.spotui-about-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);
@@ -55,6 +58,7 @@ body.spotui-about-panel #spotui-logo {
body:has(#spotui-wallpaper) body.spotui-lyrics-panel #spotui-logo, body:has(#spotui-wallpaper) body.spotui-lyrics-panel #spotui-logo,
body:has(#spotui-wallpaper) body.spotui-playlist-panel #spotui-logo, body:has(#spotui-wallpaper) body.spotui-playlist-panel #spotui-logo,
body:has(#spotui-wallpaper) body.spotui-help-panel #spotui-logo, body:has(#spotui-wallpaper) body.spotui-help-panel #spotui-logo,
body:has(#spotui-wallpaper) body.spotui-theme-panel #spotui-logo,
body:has(#spotui-wallpaper) body.spotui-about-panel #spotui-logo { body:has(#spotui-wallpaper) body.spotui-about-panel #spotui-logo {
background-color: #000; background-color: #000;
} }
@@ -136,6 +140,7 @@ body.spotui-command-mode #spotui-output,
body.spotui-playlist-panel #spotui-output, body.spotui-playlist-panel #spotui-output,
body.spotui-help-panel #spotui-output, body.spotui-help-panel #spotui-output,
body.spotui-about-panel #spotui-output, body.spotui-about-panel #spotui-output,
body.spotui-theme-panel #spotui-output,
body.spotui-lyrics-panel #spotui-output { body.spotui-lyrics-panel #spotui-output {
display: none !important; display: none !important;
} }
@@ -147,6 +152,7 @@ body.spotui-cli-mode #spotui-output {
#spotui-output::-webkit-scrollbar, #spotui-output::-webkit-scrollbar,
#spotui-help-panel::-webkit-scrollbar, #spotui-help-panel::-webkit-scrollbar,
#spotui-about-panel::-webkit-scrollbar, #spotui-about-panel::-webkit-scrollbar,
#spotui-theme-panel::-webkit-scrollbar,
#spotui-playlist-list::-webkit-scrollbar, #spotui-playlist-list::-webkit-scrollbar,
#spotui-song-list::-webkit-scrollbar, #spotui-song-list::-webkit-scrollbar,
.spotui-lyrics-lines::-webkit-scrollbar { .spotui-lyrics-lines::-webkit-scrollbar {
@@ -326,7 +332,7 @@ body.spotui-playlist-panel #spotui-playlist-panel {
transition-delay: 0.6s; transition-delay: 0.6s;
} }
#spotui-help-panel, #spotui-about-panel { #spotui-help-panel, #spotui-about-panel, #spotui-theme-panel {
display: none; display: none;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
@@ -342,10 +348,51 @@ body.spotui-playlist-panel #spotui-playlist-panel {
} }
body.spotui-help-panel #spotui-help-panel, body.spotui-help-panel #spotui-help-panel,
body.spotui-about-panel #spotui-about-panel { body.spotui-about-panel #spotui-about-panel,
body.spotui-theme-panel #spotui-theme-panel {
display: flex; display: flex;
} }
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.theme-card {
border: 1px solid #ff8c42;
border-radius: 4px;
padding: 10px;
background: rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
}
.theme-card img {
width: 100%;
height: auto;
border-radius: 4px;
object-fit: cover;
aspect-ratio: 16/9;
}
.theme-card h3 {
margin: 10px 0 5px;
color: #ff8c42;
}
.theme-card button {
background: #ff8c42;
color: #000;
border: none;
padding: 6px 12px;
font-family: "JetBrains Mono", monospace;
font-size: 13px;
cursor: pointer;
border-radius: 4px;
margin-top: auto;
}
.help-item { .help-item {
padding: 4px 0; padding: 4px 0;
display: flex; display: flex;
@@ -438,13 +485,13 @@ body.spotui-about-panel #spotui-about-panel {
} }
body.spotui-tui-hidden #spotui-tui { body.spotui-tui-hidden #spotui-tui {
display: none !important; display: none !important;
} }
body:not(.spotui-tui-hidden) .main-topBar-container, body:not(.spotui-tui-hidden) .main-topBar-container,
body:not(.spotui-tui-hidden) header { body:not(.spotui-tui-hidden) header {
display: none !important; display: none !important;
} }
`; `;
const SPOTUI_ASCII_ART = [ const SPOTUI_ASCII_ART = [
@@ -1081,6 +1128,7 @@ function createTerminal() {
</div> </div>
<div id="spotui-help-panel" hidden></div> <div id="spotui-help-panel" hidden></div>
<div id="spotui-about-panel" hidden></div> <div id="spotui-about-panel" hidden></div>
<div id="spotui-theme-panel" hidden></div>
<div id="spotui-footer"> <div id="spotui-footer">
<span class="prompt">></span> <span class="prompt">></span>
<input id="spotui-input" autofocus placeholder="type help for a list of commands"> <input id="spotui-input" autofocus placeholder="type help for a list of commands">
@@ -1091,7 +1139,7 @@ function createTerminal() {
const input = document.getElementById("spotui-input"); const input = document.getElementById("spotui-input");
input.addEventListener("keydown", async (e) => { input.addEventListener("keydown", async (e) => {
if (playlistPanelOpen) { if (playlistPanelOpen || themePanelOpen || helpPanelOpen || aboutPanelOpen) {
e.stopImmediatePropagation(); e.stopImmediatePropagation();
return; return;
} }
@@ -1150,10 +1198,10 @@ async function execute(cmd) {
if (state === "off") { if (state === "off") {
asciiEnabled = false; asciiEnabled = false;
resetGrid(); resetGrid();
try { localStorage.setItem(ANIMATION_KEY, "off"); console.log("SpoTUI: Saved off"); } catch(e) { console.error("SpoTUI: Save failed", e); } try { localStorage.setItem(ANIMATION_KEY, "off"); } catch(e) {}
} else if (state === "on") { } else if (state === "on") {
asciiEnabled = true; asciiEnabled = true;
try { localStorage.removeItem(ANIMATION_KEY); console.log("SpoTUI: Removed key"); } catch(e) { console.error("SpoTUI: Remove failed", e); } try { localStorage.removeItem(ANIMATION_KEY); } catch(e) {}
} }
return; return;
} }
@@ -1164,6 +1212,7 @@ async function execute(cmd) {
if (command === "about") { openAboutPanel(); return; } if (command === "about") { openAboutPanel(); return; }
if (command === "clear") { document.getElementById("spotui-output").textContent = ""; return; } if (command === "clear") { document.getElementById("spotui-output").textContent = ""; return; }
if (command === "playlist" || command === "list") { openPlaylistPanel(); return; } if (command === "playlist" || command === "list") { openPlaylistPanel(); return; }
if (command === "theme") { openThemePanel(); return; }
const playerMap = { const playerMap = {
play: { fn: () => { if (!Spicetify.Player.isPlaying()) Spicetify.Player.togglePlay(); }, name: "Play" }, play: { fn: () => { if (!Spicetify.Player.isPlaying()) Spicetify.Player.togglePlay(); }, name: "Play" },
@@ -1231,6 +1280,7 @@ let selectedSong = 0;
let activePane = 'playlist'; let activePane = 'playlist';
let helpPanelOpen = false; let helpPanelOpen = false;
let aboutPanelOpen = false; let aboutPanelOpen = false;
let themePanelOpen = false;
const COMMAND_LIST = [ const COMMAND_LIST = [
{ cmd: "tui -l -a &lt;on/off&gt;", desc: "Toggle ASCII animation" }, { cmd: "tui -l -a &lt;on/off&gt;", desc: "Toggle ASCII animation" },
@@ -1249,6 +1299,7 @@ const COMMAND_LIST = [
{ cmd: "search", desc: "Open Spotify search" }, { cmd: "search", desc: "Open Spotify search" },
{ cmd: "clear", desc: "Clear the TUI output" }, { cmd: "clear", desc: "Clear the TUI output" },
{ cmd: "about", desc: "Show about panel" }, { cmd: "about", desc: "Show about panel" },
{ cmd: "theme", desc: "Browse and apply themes" },
{ cmd: "help", desc: "Show this panel" }, { cmd: "help", desc: "Show this panel" },
]; ];
@@ -1264,12 +1315,14 @@ function closeActivePanel() {
if (aboutPanelOpen) setPanelState("spotui-about-panel", "spotui-about-panel", "aboutPanelOpen", false); if (aboutPanelOpen) setPanelState("spotui-about-panel", "spotui-about-panel", "aboutPanelOpen", false);
if (lyricsPanelOpen) closeLyricsPanel(); if (lyricsPanelOpen) closeLyricsPanel();
if (playlistPanelOpen) closePlaylistPanel(); if (playlistPanelOpen) closePlaylistPanel();
if (themePanelOpen) closeThemePanel();
} }
function setPanelState(panelId, className, openVarName, targetState) { function setPanelState(panelId, className, openVarName, targetState) {
const panels = { const panels = {
'helpPanelOpen': () => helpPanelOpen = targetState, 'helpPanelOpen': () => helpPanelOpen = targetState,
'aboutPanelOpen': () => aboutPanelOpen = targetState 'aboutPanelOpen': () => aboutPanelOpen = targetState,
'themePanelOpen': () => themePanelOpen = targetState,
}; };
if (panels[openVarName]) panels[openVarName](); if (panels[openVarName]) panels[openVarName]();
document.body.classList.toggle(className, targetState); document.body.classList.toggle(className, targetState);
@@ -1350,6 +1403,88 @@ async function openPlaylistPanel() {
document.addEventListener("keydown", handlePlaylistPanelKeydown); document.addEventListener("keydown", handlePlaylistPanelKeydown);
} }
function closeThemePanel() {
setPanelState("spotui-theme-panel", "spotui-theme-panel", "themePanelOpen", false);
}
async function openThemePanel() {
if (themePanelOpen) { closeThemePanel(); return; }
closeActivePanel();
setPanelState("spotui-theme-panel", "spotui-theme-panel", "themePanelOpen", true);
const panel = document.getElementById("spotui-theme-panel");
if (!panel) return;
panel.innerHTML = "<p>Loading themes...</p>";
const script = document.createElement('script');
script.src = `${THEME_HOST}themes.js`;
script.onload = () => {
const themes = window.spotuiThemes || [];
panel.innerHTML = '<div class="theme-grid"></div>';
const grid = panel.querySelector('.theme-grid');
const createCard = document.createElement('div');
createCard.className = 'theme-card';
createCard.innerHTML = `
<h3>Add yours</h3>
<img src="https://imgs.search.brave.com/qsWzCiBrdeOE9PQmFvp0eS0rfLyVkcm97DyHxEXGNBk/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9jZG4t/aWNvbnMtcG5nLm1h/Z25pZmljLmNvbS8y/NTYvMTAwODQvMTAw/ODQzOTAucG5nP3Nl/bXQ9YWlzX3doaXRl/X2xhYmVs" alt="Add Theme">
<button>Add</button>
`;
createCard.querySelector('button').addEventListener('click', () => {
window.open('https://spotui.root.sx/', '_blank');
});
grid.appendChild(createCard);
themes.forEach(theme => {
const card = document.createElement('div');
card.className = 'theme-card';
card.innerHTML = `
<h3>${theme.name}</h3>
<img src="${theme.screenshot_url}" alt="${theme.name} screenshot">
<button data-commands='${JSON.stringify(theme.commands)}'>Apply</button>
`;
grid.appendChild(card);
});
grid.addEventListener('click', e => {
if (e.target.tagName === 'BUTTON' && e.target.dataset.commands) {
const commands = JSON.parse(e.target.dataset.commands);
commands.forEach(cmd => execute(cmd));
closeThemePanel();
}
});
document.body.removeChild(script);
};
script.onerror = () => {
panel.innerHTML = `<p>¯\\_(ツ)_/¯</p><p>Error loading themes. The server may be down or you are rate-limited. Please wait and try again.</p>`;
const grid = document.createElement('div');
grid.className = 'theme-grid';
const createCard = document.createElement('div');
createCard.className = 'theme-card';
createCard.innerHTML = `
<h3>Add yours</h3>
<img src="https://imgs.search.brave.com/qsWzCiBrdeOE9PQmFvp0eS0rfLyVkcm97DyHxEXGNBk/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9jZG4t/aWNvbnMtcG5nLm1h/Z25pZmljLmNvbS8y/NTYvMTAwODQvMTAw/ODQzOTAucG5nP3Nl/bXQ9YWlzX3doaXRl/X2xhYmVs" alt="Add Theme">
<button>Add</button>
`;
createCard.querySelector('button').addEventListener('click', () => {
window.open('https://spotui.root.sx/', '_blank');
});
grid.appendChild(createCard);
panel.appendChild(grid);
document.body.removeChild(script);
};
document.body.appendChild(script);
}
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");
@@ -1529,6 +1664,7 @@ async function getPlaylists() {
const LYRICS_STORAGE_KEY = "spotui:lyrics-open"; const LYRICS_STORAGE_KEY = "spotui:lyrics-open";
const WP_URL_KEY = "spotui:wp-url"; const WP_URL_KEY = "spotui:wp-url";
const WP_OPACITY_KEY = "spotui:wp-opacity"; const WP_OPACITY_KEY = "spotui:wp-opacity";
let lyricsPanelOpen = false; let lyricsPanelOpen = false;
let lyricsLoadToken = 0; let lyricsLoadToken = 0;
let lyricsActiveIndex = -1; let lyricsActiveIndex = -1;