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); + } + }); +})();