diff --git a/public/core.js b/public/core.js
index 16a743a..4f10a10 100644
--- a/public/core.js
+++ b/public/core.js
@@ -26,6 +26,7 @@ window.MusicRoom = {
preMuteVolume: 1,
STORAGE_KEY: "blastoise_volume",
CHANNEL_STORAGE_KEY: "blastoise_channel",
+ THEME_STORAGE_KEY: "blastoise_theme",
// Playback state
localTimestamp: 0,
@@ -71,17 +72,25 @@ window.MusicRoom = {
const M = window.MusicRoom;
const CHANNEL_COOKIE_MAX_AGE = 60 * 60 * 24 * 400;
- function getCookie(name) {
+ M.getCookieValue = function(name) {
const prefix = `${name}=`;
return document.cookie
.split(";")
.map(cookie => cookie.trim())
.find(cookie => cookie.startsWith(prefix))
?.slice(prefix.length) || null;
- }
+ };
+
+ M.setCookieValue = function(name, value, maxAge = CHANNEL_COOKIE_MAX_AGE) {
+ document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=${maxAge}`;
+ };
+
+ M.clearCookieValue = function(name) {
+ document.cookie = `${name}=; Path=/; SameSite=Lax; Max-Age=0`;
+ };
M.getSavedChannelId = function() {
- const cookieValue = getCookie(M.CHANNEL_STORAGE_KEY);
+ const cookieValue = M.getCookieValue(M.CHANNEL_STORAGE_KEY);
if (cookieValue) {
try {
return decodeURIComponent(cookieValue);
@@ -103,8 +112,7 @@ window.MusicRoom = {
M.rememberChannel = function(channelId) {
if (!channelId) return;
- const encoded = encodeURIComponent(channelId);
- document.cookie = `${M.CHANNEL_STORAGE_KEY}=${encoded}; Path=/; SameSite=Lax; Max-Age=${CHANNEL_COOKIE_MAX_AGE}`;
+ M.setCookieValue(M.CHANNEL_STORAGE_KEY, channelId);
try {
localStorage.setItem(M.CHANNEL_STORAGE_KEY, channelId);
@@ -112,7 +120,7 @@ window.MusicRoom = {
};
M.clearRememberedChannel = function() {
- document.cookie = `${M.CHANNEL_STORAGE_KEY}=; Path=/; SameSite=Lax; Max-Age=0`;
+ M.clearCookieValue(M.CHANNEL_STORAGE_KEY);
try {
localStorage.removeItem(M.CHANNEL_STORAGE_KEY);
diff --git a/public/index.html b/public/index.html
index 181dbe0..9e1e80b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -47,7 +47,29 @@
-
+
+
+
@@ -179,6 +201,7 @@
+
diff --git a/public/styles.css b/public/styles.css
index 6a3d5d8..624e5d7 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -662,3 +662,399 @@ button:hover { background: #333; }
/* Login panel */
#login-panel { padding: 1rem; }
}
+
+/* Theme selector */
+#stream-select { margin-left: auto; }
+#theme-select { min-width: 170px; }
+
+/* Shared theme surfaces */
+body[data-ui-theme]:not([data-ui-theme="default"]) #channels-panel,
+body[data-ui-theme]:not([data-ui-theme="default"]) #library-panel,
+body[data-ui-theme]:not([data-ui-theme="default"]) #queue-panel,
+body[data-ui-theme]:not([data-ui-theme="default"]) #player-bar,
+body[data-ui-theme]:not([data-ui-theme="default"]) #login-panel,
+body[data-ui-theme]:not([data-ui-theme="default"]) .panel-views,
+body[data-ui-theme]:not([data-ui-theme="default"]) #playlist-contents-header,
+body[data-ui-theme]:not([data-ui-theme="default"]) .context-menu,
+body[data-ui-theme]:not([data-ui-theme="default"]) #toast-history {
+ background: var(--panel-bg, #1a1a1a);
+ border-color: var(--panel-border, #333);
+ color: var(--text-color, #eee);
+}
+
+body[data-ui-theme]:not([data-ui-theme="default"]) #site-header h1,
+body[data-ui-theme]:not([data-ui-theme="default"]) h3,
+body[data-ui-theme]:not([data-ui-theme="default"]) .panel-tab.active,
+body[data-ui-theme]:not([data-ui-theme="default"]) #selected-playlist-name {
+ color: var(--heading-color, var(--text-color, #eee));
+}
+
+body[data-ui-theme]:not([data-ui-theme="default"]) button,
+body[data-ui-theme]:not([data-ui-theme="default"]) select,
+body[data-ui-theme]:not([data-ui-theme="default"]) input,
+body[data-ui-theme]:not([data-ui-theme="default"]) .panel-tab,
+body[data-ui-theme]:not([data-ui-theme="default"]) .track-actions .track-menu-btn,
+body[data-ui-theme]:not([data-ui-theme="default"]) .track-actions .track-play-btn,
+body[data-ui-theme]:not([data-ui-theme="default"]) .track-actions .track-preview-btn {
+ background: var(--control-bg, #222);
+ border-color: var(--control-border, #444);
+ color: var(--control-color, var(--text-color, #eee));
+}
+
+body[data-ui-theme]:not([data-ui-theme="default"]) .track:hover,
+body[data-ui-theme]:not([data-ui-theme="default"]) .playlist-item:hover,
+body[data-ui-theme]:not([data-ui-theme="default"]) .context-menu-item:hover {
+ background: var(--hover-bg, #222);
+}
+
+body[data-ui-theme]:not([data-ui-theme="default"]) #queue .track.active,
+body[data-ui-theme]:not([data-ui-theme="default"]) .playlist-item.selected,
+body[data-ui-theme]:not([data-ui-theme="default"]) .mobile-tab.active,
+body[data-ui-theme]:not([data-ui-theme="default"]) .now-playing-bar,
+body[data-ui-theme]:not([data-ui-theme="default"]) .toast {
+ background: var(--active-bg, #2a4a3a);
+ border-color: var(--accent, #4e8);
+ color: var(--active-color, var(--accent, #4e8));
+}
+
+body[data-ui-theme]:not([data-ui-theme="default"]) .track.cached .cache-indicator,
+body[data-ui-theme]:not([data-ui-theme="default"]) #progress-bar.playing.synced,
+body[data-ui-theme]:not([data-ui-theme="default"]) #buffer-bar .segment.available,
+body[data-ui-theme]:not([data-ui-theme="default"]) #sync-indicator.visible {
+ background: var(--accent, #4e8);
+}
+
+body[data-ui-theme]:not([data-ui-theme="default"]) .track.not-cached .cache-indicator,
+body[data-ui-theme]:not([data-ui-theme="default"]) #btn-sync.synced,
+body[data-ui-theme]:not([data-ui-theme="default"]) .toast.toast-warning {
+ color: var(--warning, #ea4);
+}
+
+body[data-ui-theme="phosphor"] {
+ --panel-bg: #07140f;
+ --panel-border: #1f7a48;
+ --text-color: #caf7d9;
+ --heading-color: #73ff9c;
+ --control-bg: #0b1f17;
+ --control-border: #24965a;
+ --control-color: #dfffe8;
+ --hover-bg: #123321;
+ --active-bg: #123d29;
+ --active-color: #8cffad;
+ --accent: #51f080;
+ --warning: #f5de72;
+ background: #020806;
+ color: var(--text-color);
+}
+
+body[data-ui-theme="phosphor"] #app { filter: drop-shadow(0 0 8px rgba(81, 240, 128, 0.12)); }
+body[data-ui-theme="phosphor"] #track-name,
+body[data-ui-theme="phosphor"] #btn-sync.synced.connected { text-shadow: 0 0 10px rgba(81, 240, 128, 0.55); }
+
+body[data-ui-theme="vaporwave"] {
+ --panel-bg: #21123a;
+ --panel-border: #ff71ce;
+ --text-color: #f8d7ff;
+ --heading-color: #05ffa1;
+ --control-bg: #351a54;
+ --control-border: #b967ff;
+ --control-color: #ffffff;
+ --hover-bg: #46206d;
+ --active-bg: #2d567a;
+ --active-color: #05ffa1;
+ --accent: #01cdfe;
+ --warning: #fffb96;
+ background: linear-gradient(135deg, #12091f 0%, #2f1856 46%, #0b4857 100%);
+ color: var(--text-color);
+}
+
+body[data-ui-theme="vaporwave"] #main-content { gap: 0.8rem; }
+body[data-ui-theme="vaporwave"] #channels-panel,
+body[data-ui-theme="vaporwave"] #library-panel,
+body[data-ui-theme="vaporwave"] #queue-panel,
+body[data-ui-theme="vaporwave"] #player-bar { box-shadow: 0 0 22px rgba(1, 205, 254, 0.2), inset 0 0 0 1px rgba(255, 113, 206, 0.22); }
+
+body[data-ui-theme="sunrise"] {
+ --panel-bg: #241b2d;
+ --panel-border: #f18f01;
+ --text-color: #fff0d4;
+ --heading-color: #ffcf56;
+ --control-bg: #3a2340;
+ --control-border: #f45d48;
+ --control-color: #fff7e8;
+ --hover-bg: #50314f;
+ --active-bg: #5f342e;
+ --active-color: #ffd166;
+ --accent: #ff9f1c;
+ --warning: #2ec4b6;
+ background: radial-gradient(circle at 20% 0%, #7a2f5c 0, #2b1838 45%, #101018 100%);
+ color: var(--text-color);
+}
+
+body[data-ui-theme="paper"] {
+ --panel-bg: #f4efe3;
+ --panel-border: #3b3327;
+ --text-color: #1d1a17;
+ --heading-color: #0b0b0b;
+ --control-bg: #fffaf0;
+ --control-border: #5a4a36;
+ --control-color: #1d1a17;
+ --hover-bg: #e5d8bd;
+ --active-bg: #d9c69f;
+ --active-color: #1d1a17;
+ --accent: #285f47;
+ --warning: #a75a00;
+ background: #d7c9aa;
+ color: var(--text-color);
+}
+
+body[data-ui-theme="paper"] #channels-panel,
+body[data-ui-theme="paper"] #library-panel,
+body[data-ui-theme="paper"] #queue-panel,
+body[data-ui-theme="paper"] #player-bar {
+ border: 1px solid var(--panel-border);
+ box-shadow: 4px 4px 0 #3b3327;
+}
+
+body[data-ui-theme="contrast"] {
+ --panel-bg: #000;
+ --panel-border: #fff;
+ --text-color: #fff;
+ --heading-color: #ffff00;
+ --control-bg: #000;
+ --control-border: #fff;
+ --control-color: #fff;
+ --hover-bg: #202020;
+ --active-bg: #ffff00;
+ --active-color: #000;
+ --accent: #00ffff;
+ --warning: #ff00ff;
+ background: #000;
+ color: #fff;
+}
+
+body[data-ui-theme="contrast"] #channels-panel,
+body[data-ui-theme="contrast"] #library-panel,
+body[data-ui-theme="contrast"] #queue-panel,
+body[data-ui-theme="contrast"] #player-bar { border: 2px solid #fff; border-radius: 0; }
+
+body[data-ui-theme="aquarium"] {
+ --panel-bg: rgba(8, 42, 58, 0.86);
+ --panel-border: #52e0c4;
+ --text-color: #d7fff8;
+ --heading-color: #a3fff1;
+ --control-bg: #10394d;
+ --control-border: #59bde0;
+ --control-color: #ebfffb;
+ --hover-bg: #174d62;
+ --active-bg: #0d5f65;
+ --active-color: #adfff0;
+ --accent: #52e0c4;
+ --warning: #ffd166;
+ background: linear-gradient(180deg, #062c45, #03161d);
+ color: var(--text-color);
+}
+
+body[data-ui-theme="aquarium"] #app { backdrop-filter: blur(2px); }
+body[data-ui-theme="aquarium"] #channels-panel,
+body[data-ui-theme="aquarium"] #library-panel,
+body[data-ui-theme="aquarium"] #queue-panel,
+body[data-ui-theme="aquarium"] #player-bar { border: 1px solid rgba(163, 255, 241, 0.42); }
+
+body[data-ui-theme="amber"] {
+ --panel-bg: #1b1204;
+ --panel-border: #8c5d13;
+ --text-color: #ffd98a;
+ --heading-color: #ffb000;
+ --control-bg: #2a1b06;
+ --control-border: #b97919;
+ --control-color: #ffe8ad;
+ --hover-bg: #3a2508;
+ --active-bg: #4c3109;
+ --active-color: #ffc857;
+ --accent: #ffb000;
+ --warning: #f77f00;
+ background: #080502;
+ color: var(--text-color);
+ font-family: "Cascadia Mono", "Consolas", monospace;
+}
+
+body[data-ui-theme="amber"] * { letter-spacing: 0; }
+body[data-ui-theme="amber"] #track-name { text-transform: uppercase; }
+
+body[data-ui-theme="compact"] {
+ --panel-bg: #15181b;
+ --panel-border: #35414a;
+ --text-color: #e6edf3;
+ --heading-color: #9fc7ff;
+ --control-bg: #20262c;
+ --control-border: #3e4d58;
+ --control-color: #edf4fb;
+ --hover-bg: #242c33;
+ --active-bg: #22364b;
+ --active-color: #9fc7ff;
+ --accent: #79c0ff;
+ --warning: #e3b341;
+ background: #0d1117;
+ color: var(--text-color);
+}
+
+body[data-ui-theme="compact"] #app { max-width: 1200px; padding: 0.25rem; }
+body[data-ui-theme="compact"] #main-content { gap: 0.25rem; }
+body[data-ui-theme="compact"] #channels-panel { flex-basis: 130px; }
+body[data-ui-theme="compact"] #library-panel,
+body[data-ui-theme="compact"] #queue-panel { padding: 0.25rem; max-height: 66vh; }
+body[data-ui-theme="compact"] #library .track,
+body[data-ui-theme="compact"] #queue .track,
+body[data-ui-theme="compact"] #playlist-tracks .track { padding: 0.16rem 0.35rem; font-size: 0.78rem; }
+body[data-ui-theme="compact"] #player-bar { padding: 0.35rem 0.5rem; }
+
+body[data-ui-theme="cinema"] {
+ --panel-bg: #101010;
+ --panel-border: #41362a;
+ --text-color: #e8dfcc;
+ --heading-color: #f6c65b;
+ --control-bg: #1f1a15;
+ --control-border: #6d5735;
+ --control-color: #f3ead7;
+ --hover-bg: #292118;
+ --active-bg: #3d2b18;
+ --active-color: #f6c65b;
+ --accent: #f6c65b;
+ --warning: #db5461;
+ background: #050505;
+ color: var(--text-color);
+}
+
+body[data-ui-theme="cinema"] #main-content { display: grid; grid-template-columns: 150px minmax(280px, 0.8fr) minmax(420px, 1.45fr); }
+body[data-ui-theme="cinema"] #queue-panel { order: 3; max-height: 70vh; }
+body[data-ui-theme="cinema"] #queue .track.active { font-size: 1rem; padding: 0.65rem 0.75rem; }
+body[data-ui-theme="cinema"] #player-bar { border-top: 4px solid var(--accent); }
+
+html[data-ui-theme="spreadsheet"],
+html[data-ui-theme="myspace"],
+html[data-ui-theme="sideways"] {
+ max-width: none;
+ overflow-x: auto;
+ overscroll-behavior-x: auto;
+}
+
+html[data-ui-theme="spreadsheet"] { min-width: 2100px; }
+html[data-ui-theme="myspace"] { min-width: 1850px; }
+html[data-ui-theme="sideways"] { min-width: 2600px; }
+
+body[data-ui-theme="spreadsheet"],
+body[data-ui-theme="myspace"],
+body[data-ui-theme="sideways"] {
+ max-width: none;
+ overflow-x: auto;
+}
+
+body[data-ui-theme="spreadsheet"] {
+ --panel-bg: #ffffff;
+ --panel-border: #8b8b8b;
+ --text-color: #000000;
+ --heading-color: #008000;
+ --control-bg: #efefef;
+ --control-border: #777777;
+ --control-color: #000000;
+ --hover-bg: #ffffcc;
+ --active-bg: #00ff00;
+ --active-color: #000000;
+ --accent: #0000ff;
+ --warning: #ff0000;
+ background: #fff;
+ color: #000;
+ font-family: "Times New Roman", serif;
+}
+
+body[data-ui-theme="spreadsheet"] #app { width: 2100px; max-width: none; overflow-x: visible; }
+body[data-ui-theme="spreadsheet"] #main-content { width: 2050px; max-width: none; overflow-x: visible; gap: 0; }
+body[data-ui-theme="spreadsheet"] #channels-panel,
+body[data-ui-theme="spreadsheet"] #library-panel,
+body[data-ui-theme="spreadsheet"] #queue-panel,
+body[data-ui-theme="spreadsheet"] #player-bar {
+ border: 1px solid #000;
+ border-radius: 0;
+ box-shadow: none;
+}
+body[data-ui-theme="spreadsheet"] .track { border-bottom: 1px solid #999; border-radius: 0; }
+body[data-ui-theme="spreadsheet"] #library,
+body[data-ui-theme="spreadsheet"] #queue { overflow-x: scroll; }
+
+body[data-ui-theme="myspace"] {
+ --panel-bg: #220044;
+ --panel-border: #00ff66;
+ --text-color: #ffff00;
+ --heading-color: #ff00ff;
+ --control-bg: #0000cc;
+ --control-border: #ffff00;
+ --control-color: #ffffff;
+ --hover-bg: #ff00ff;
+ --active-bg: #00ffff;
+ --active-color: #000000;
+ --accent: #00ff66;
+ --warning: #ff6600;
+ background: repeating-linear-gradient(45deg, #ff00ff 0 18px, #00ffff 18px 36px, #ffff00 36px 54px);
+ color: var(--text-color);
+ font-family: Impact, "Arial Black", sans-serif;
+}
+
+body[data-ui-theme="myspace"] #app { width: 1850px; max-width: none; overflow-x: visible; transform: rotate(-0.35deg); }
+body[data-ui-theme="myspace"] #main-content { width: 1780px; max-width: none; overflow-x: visible; gap: 1.25rem; }
+body[data-ui-theme="myspace"] #channels-panel,
+body[data-ui-theme="myspace"] #library-panel,
+body[data-ui-theme="myspace"] #queue-panel,
+body[data-ui-theme="myspace"] #player-bar {
+ border: 6px ridge #00ff66;
+ border-radius: 22px;
+ box-shadow: 12px 12px 0 #ff00ff, -9px -7px 0 #00ffff;
+}
+body[data-ui-theme="myspace"] .track:nth-child(odd) { transform: rotate(0.45deg); }
+body[data-ui-theme="myspace"] .track:nth-child(even) { transform: rotate(-0.45deg); }
+body[data-ui-theme="myspace"] #track-name { font-size: 1.25rem; color: #00ff66; text-shadow: 2px 2px #ff00ff; }
+
+body[data-ui-theme="sideways"] {
+ --panel-bg: #241313;
+ --panel-border: #cc3333;
+ --text-color: #f8dcdc;
+ --heading-color: #ff6b35;
+ --control-bg: #351b1b;
+ --control-border: #8c2f39;
+ --control-color: #fff1f1;
+ --hover-bg: #482121;
+ --active-bg: #5a231f;
+ --active-color: #ffb199;
+ --accent: #ff6b35;
+ --warning: #ffd166;
+ background: #190b0b;
+ color: var(--text-color);
+}
+
+body[data-ui-theme="sideways"] #app { width: 2600px; max-width: none; overflow-x: visible; }
+body[data-ui-theme="sideways"] #main-content {
+ width: 2480px;
+ max-width: none;
+ display: grid;
+ grid-template-columns: 260px 1060px 1060px;
+ overflow-x: visible;
+}
+body[data-ui-theme="sideways"] #channels-panel,
+body[data-ui-theme="sideways"] #library-panel,
+body[data-ui-theme="sideways"] #queue-panel { max-height: 42vh; }
+body[data-ui-theme="sideways"] #player-bar { width: 2480px; max-width: none; }
+
+@media (max-width: 768px) {
+ #stream-select { width: 100%; margin-left: 0; }
+ #theme-select { width: 100%; min-height: 44px; }
+
+ body[data-ui-theme="compact"] #app,
+ body[data-ui-theme="cinema"] #app {
+ max-width: 100%;
+ }
+
+ body[data-ui-theme="cinema"] #main-content {
+ display: flex;
+ grid-template-columns: none;
+ }
+}
diff --git a/public/themes.js b/public/themes.js
new file mode 100644
index 0000000..b68b6eb
--- /dev/null
+++ b/public/themes.js
@@ -0,0 +1,90 @@
+// MusicRoom - UI theme and layout experiments
+
+(function() {
+ const M = window.MusicRoom;
+
+ const THEMES = new Set([
+ "default",
+ "phosphor",
+ "vaporwave",
+ "sunrise",
+ "paper",
+ "contrast",
+ "aquarium",
+ "amber",
+ "compact",
+ "cinema",
+ "spreadsheet",
+ "myspace",
+ "sideways"
+ ]);
+
+ function decodeCookieValue(value) {
+ if (!value) return null;
+ try {
+ return decodeURIComponent(value);
+ } catch {
+ M.clearCookieValue(M.THEME_STORAGE_KEY);
+ return null;
+ }
+ }
+
+ function normalizeTheme(theme) {
+ return THEMES.has(theme) ? theme : "default";
+ }
+
+ M.getSavedTheme = function() {
+ const cookieTheme = normalizeTheme(decodeCookieValue(M.getCookieValue(M.THEME_STORAGE_KEY)));
+ if (cookieTheme !== "default") return cookieTheme;
+
+ try {
+ const storedTheme = normalizeTheme(localStorage.getItem(M.THEME_STORAGE_KEY));
+ if (storedTheme !== "default") {
+ M.rememberTheme(storedTheme);
+ return storedTheme;
+ }
+ } catch {}
+
+ return "default";
+ };
+
+ M.rememberTheme = function(theme) {
+ const normalized = normalizeTheme(theme);
+ if (normalized === "default") {
+ M.clearThemePreference();
+ return;
+ }
+
+ M.setCookieValue(M.THEME_STORAGE_KEY, normalized);
+ try {
+ localStorage.setItem(M.THEME_STORAGE_KEY, normalized);
+ } catch {}
+ };
+
+ M.clearThemePreference = function() {
+ M.clearCookieValue(M.THEME_STORAGE_KEY);
+ try {
+ localStorage.removeItem(M.THEME_STORAGE_KEY);
+ } catch {}
+ };
+
+ M.applyTheme = function(theme, remember = true) {
+ const normalized = normalizeTheme(theme);
+ document.documentElement.dataset.uiTheme = normalized;
+ document.body.dataset.uiTheme = normalized;
+
+ const selector = document.getElementById("theme-select");
+ if (selector) selector.value = normalized;
+
+ if (remember) M.rememberTheme(normalized);
+ };
+
+ document.addEventListener("DOMContentLoaded", () => {
+ const selector = document.getElementById("theme-select");
+ M.applyTheme(M.getSavedTheme(), false);
+
+ if (selector) {
+ selector.onchange = () => M.applyTheme(selector.value);
+ }
+ });
+})();