forked from SpoTUI/SpoTUI
feat(theme): Add theme browser /theme
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 <on/off>", desc: "Toggle ASCII animation" },
|
{ cmd: "tui -l -a <on/off>", 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user