From aef6b9590e9c2931a00bbdb22b1aaee2fc645d74 Mon Sep 17 00:00:00 2001 From: peterino2 Date: Thu, 2 Jul 2026 13:19:41 -0700 Subject: [PATCH] Improve responsive queue layout and remember channels --- public/channelSync.js | 8 +++-- public/core.js | 54 ++++++++++++++++++++++++++++++++ public/index.html | 2 +- public/queue.js | 16 +++++++++- public/styles.css | 67 +++++++++++++++++++++++++++++++++------- public/trackContainer.js | 4 +-- public/ui.js | 2 +- 7 files changed, 134 insertions(+), 19 deletions(-) diff --git a/public/channelSync.js b/public/channelSync.js index 3611abf..a90fc24 100644 --- a/public/channelSync.js +++ b/public/channelSync.js @@ -16,7 +16,7 @@ M.channels = channels; M.renderChannelList(); // Try saved channel first, fall back to default - const savedChannelId = localStorage.getItem("blastoise_channel"); + const savedChannelId = M.getSavedChannelId(); const savedChannel = savedChannelId && channels.find(c => c.id === savedChannelId); const targetChannel = savedChannel || channels.find(c => c.isDefault) || channels[0]; M.connectChannel(targetChannel.id); @@ -66,6 +66,9 @@ M.showToast(err.error || "Failed to delete channel", "error"); return; } + if (M.getSavedChannelId() === channelId) { + M.clearRememberedChannel(); + } M.showToast(`Channel "${channel.name}" deleted`); } catch (e) { M.showToast("Failed to delete channel", "error"); @@ -247,7 +250,7 @@ oldWs.close(); } M.currentChannelId = id; - localStorage.setItem("blastoise_channel", id); + M.rememberChannel(id); const proto = location.protocol === "https:" ? "wss:" : "ws:"; M.ws = new WebSocket(proto + "//" + location.host + "/api/channels/" + id + "/ws"); @@ -265,6 +268,7 @@ // Handle channel switch confirmation if (data.type === "switched") { M.currentChannelId = data.channelId; + M.rememberChannel(data.channelId); M.renderChannelList(); return; } diff --git a/public/core.js b/public/core.js index 9de1c45..16a743a 100644 --- a/public/core.js +++ b/public/core.js @@ -25,6 +25,7 @@ window.MusicRoom = { // Volume preMuteVolume: 1, STORAGE_KEY: "blastoise_volume", + CHANNEL_STORAGE_KEY: "blastoise_channel", // Playback state localTimestamp: 0, @@ -65,3 +66,56 @@ window.MusicRoom = { lastBufferPct: -1, lastSpeedText: "" }; + +(function() { + const M = window.MusicRoom; + const CHANNEL_COOKIE_MAX_AGE = 60 * 60 * 24 * 400; + + function getCookie(name) { + const prefix = `${name}=`; + return document.cookie + .split(";") + .map(cookie => cookie.trim()) + .find(cookie => cookie.startsWith(prefix)) + ?.slice(prefix.length) || null; + } + + M.getSavedChannelId = function() { + const cookieValue = getCookie(M.CHANNEL_STORAGE_KEY); + if (cookieValue) { + try { + return decodeURIComponent(cookieValue); + } catch { + M.clearRememberedChannel(); + } + } + + try { + const storedValue = localStorage.getItem(M.CHANNEL_STORAGE_KEY); + if (storedValue) { + M.rememberChannel(storedValue); + return storedValue; + } + } catch {} + + return null; + }; + + 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}`; + + try { + localStorage.setItem(M.CHANNEL_STORAGE_KEY, channelId); + } catch {} + }; + + M.clearRememberedChannel = function() { + document.cookie = `${M.CHANNEL_STORAGE_KEY}=; Path=/; SameSite=Lax; Max-Age=0`; + + try { + localStorage.removeItem(M.CHANNEL_STORAGE_KEY); + } catch {} + }; +})(); diff --git a/public/index.html b/public/index.html index 671b35e..181dbe0 100644 --- a/public/index.html +++ b/public/index.html @@ -53,8 +53,8 @@
- +
diff --git a/public/queue.js b/public/queue.js index 1fd4792..1dc0494 100644 --- a/public/queue.js +++ b/public/queue.js @@ -282,7 +282,21 @@ const activeTrack = container.querySelector(".track.active"); if (activeTrack) { - activeTrack.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" }); + const containerRect = container.getBoundingClientRect(); + const trackRect = activeTrack.getBoundingClientRect(); + const trackTop = trackRect.top - containerRect.top + container.scrollTop; + const targetTop = trackTop - (container.clientHeight - activeTrack.offsetHeight) / 2; + const maxTop = Math.max(0, container.scrollHeight - container.clientHeight); + + container.scrollTo({ + top: Math.min(Math.max(0, targetTop), maxTop), + behavior: "smooth" + }); + container.scrollLeft = 0; + + if (window.scrollX !== 0) { + window.scrollTo({ left: 0, top: window.scrollY, behavior: "auto" }); + } } }; diff --git a/public/styles.css b/public/styles.css index 81b7e93..6a3d5d8 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,6 +1,7 @@ * { margin: 0; padding: 0; box-sizing: border-box; } -body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #111; color: #eee; min-height: 100vh; overflow-x: hidden; } -#app { width: 100%; max-width: 1700px; margin: 0 auto; padding: 0.5rem; display: flex; flex-direction: column; min-height: 100vh; } +html { width: 100%; max-width: 100%; overflow-x: hidden; overscroll-behavior-x: none; } +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #111; color: #eee; width: 100%; max-width: 100%; min-height: 100vh; overflow-x: hidden; } +#app { width: 100%; max-width: 1700px; min-width: 0; margin: 0 auto; padding: 0.5rem; display: flex; flex-direction: column; min-height: 100vh; overflow-x: hidden; } h1 { font-size: 1rem; color: #aaa; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.4rem; } h3 { font-size: 0.8rem; color: #999; margin-bottom: 0.3rem; text-transform: uppercase; letter-spacing: 0.05em; } #sync-indicator { width: 8px; height: 8px; border-radius: 50%; background: #4e8; display: none; flex-shrink: 0; } @@ -26,8 +27,8 @@ h3 { font-size: 0.8rem; color: #999; margin-bottom: 0.3rem; text-transform: uppe #stream-select select { background: #222; color: #eee; border: 1px solid #333; padding: 0.3rem 0.6rem; border-radius: 4px; font-size: 0.85rem; } /* Main content - library and queue */ -#main-content { display: flex; gap: 0.5rem; flex: 1; width: 100%; min-width: 0; min-height: 0; margin-bottom: 0.5rem; max-width: 1600px; margin-left: auto; margin-right: auto; } -#channels-panel { flex: 0 0 160px; background: #1a1a1a; border-radius: 6px; padding: 0.4rem; display: flex; flex-direction: column; min-height: 250px; max-height: 60vh; } +#main-content { display: flex; gap: 0.5rem; flex: 1; width: 100%; min-width: 0; min-height: 0; margin-bottom: 0.5rem; max-width: 1600px; margin-left: auto; margin-right: auto; overflow-x: hidden; } +#channels-panel { order: 1; flex: 0 0 160px; min-width: 0; background: #1a1a1a; border-radius: 6px; padding: 0.4rem; display: flex; flex-direction: column; min-height: 250px; max-height: 60vh; } #channels-list { flex: 1; overflow-y: auto; } #channels-list .channel-item { padding: 0.2rem 0.4rem; border-radius: 3px; font-size: 0.8rem; display: flex; flex-direction: column; gap: 0.1rem; } #channels-list .channel-item.active { background: #2a4a3a; color: #4e8; } @@ -46,7 +47,9 @@ h3 { font-size: 0.8rem; color: #999; margin-bottom: 0.3rem; text-transform: uppe #channels-list .listener { font-size: 0.65rem; color: #aaa; padding: 0.05rem 0; position: relative; } #channels-list .listener::before { content: ""; position: absolute; left: -0.3rem; top: 50%; width: 0.2rem; height: 1px; background: #333; } #channels-list .listener-mult { color: #999; font-size: 0.55rem; } -#library-panel, #queue-panel { flex: 1 1 0; min-width: 0; overflow: hidden; background: #1a1a1a; border-radius: 6px; padding: 0.5rem; display: flex; flex-direction: column; min-height: 250px; max-height: 60vh; position: relative; } +#library-panel, #queue-panel { flex: 1 1 0; min-width: 0; max-width: 100%; overflow: hidden; background: #1a1a1a; border-radius: 6px; padding: 0.5rem; display: flex; flex-direction: column; min-height: 250px; max-height: 60vh; position: relative; } +#library-panel { order: 2; } +#queue-panel { order: 3; } .panel-tabs { display: flex; gap: 0; margin-bottom: 0; flex-shrink: 0; } .panel-tab { background: #252525; border: none; color: #999; font-family: inherit; font-size: 0.8rem; font-weight: bold; padding: 0.3rem 0.6rem; cursor: pointer; border-radius: 4px 4px 0 0; text-transform: uppercase; letter-spacing: 0.05em; margin-right: 2px; } .panel-tab:hover { color: #aaa; background: #2a2a2a; } @@ -193,14 +196,14 @@ h3 { font-size: 0.8rem; color: #999; margin-bottom: 0.3rem; text-transform: uppe .context-menu-item.danger:hover { background: #3a2a2a; } /* Player bar */ -#player-bar { background: #1a1a1a; border-radius: 6px; padding: 0.5rem 0.75rem; display: flex; gap: 0.75rem; align-items: center; } -#now-playing { width: 180px; flex-shrink: 0; } +#player-bar { background: #1a1a1a; border-radius: 6px; padding: 0.5rem 0.75rem; display: flex; gap: 0.75rem; align-items: center; min-width: 0; max-width: 100%; overflow-x: hidden; } +#now-playing { width: 180px; flex: 0 1 180px; min-width: 0; } #channel-name { font-size: 0.7rem; color: #666; margin-bottom: 0.1rem; } -#track-name { font-size: 0.9rem; font-weight: 600; overflow: hidden; position: relative; } +#track-name { font-size: 0.9rem; font-weight: 600; overflow: hidden; position: relative; min-width: 0; } #track-name .marquee-inner { display: inline-block; white-space: nowrap; } #track-name.scrolling .marquee-inner { animation: scroll-marquee 8s linear infinite; } @keyframes scroll-marquee { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } -#player-controls { flex: 1; } +#player-controls { flex: 1 1 auto; min-width: 0; } #progress-row { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.2rem; } #progress-row.denied { animation: flash-red 0.5s ease-out; } @keyframes flash-red { 0% { background: #e44; } 100% { background: transparent; } } @@ -218,7 +221,7 @@ h3 { font-size: 0.8rem; color: #999; margin-bottom: 0.3rem; text-transform: uppe #btn-mode:hover { opacity: 0.8; } #status-icon { font-size: 0.85rem; width: 1rem; text-align: center; cursor: pointer; } .control-disabled { opacity: 0.45 !important; cursor: not-allowed !important; } -#progress-container { background: #222; border-radius: 4px; height: 5px; cursor: pointer; position: relative; flex: 1; } +#progress-container { background: #222; border-radius: 4px; height: 5px; cursor: pointer; position: relative; flex: 1; min-width: 0; } #progress-bar { background: #555; height: 100%; border-radius: 4px; width: 0%; transition: width 0.3s linear; pointer-events: none; } #progress-bar.playing.synced { background: #4e8; } #progress-bar.playing.local { background: #c4f; } @@ -231,7 +234,7 @@ h3 { font-size: 0.8rem; color: #999; margin-bottom: 0.3rem; text-transform: uppe #buffer-bar .segment.loading { background: #666; animation: throb 0.6s ease-in-out infinite alternate; } @keyframes throb { from { background: #444; } to { background: #888; } } #download-speed { font-size: 0.6rem; color: #888; text-align: right; } -#volume-controls { display: flex; gap: 0.4rem; align-items: center; } +#volume-controls { display: flex; gap: 0.4rem; align-items: center; min-width: 0; } #btn-stream-only { font-size: 0.7rem; cursor: pointer; color: #666; transition: color 0.2s, text-shadow 0.2s; letter-spacing: 0.05em; } #btn-stream-only:hover { color: #888; } #btn-stream-only.active { color: #4af; text-shadow: 0 0 6px #4af; } @@ -344,6 +347,43 @@ button:hover { background: #333; } /* Mobile tab bar - hidden on desktop */ #mobile-tabs { display: none; } +/* Medium-width layout: keep the queue closer to the user's hand */ +@media (max-width: 1180px) { + #main-content { + max-width: 100%; + } + + #channels-panel { + flex-basis: 145px; + } + + #queue-panel { + order: 2; + flex-grow: 1.12; + } + + #library-panel { + order: 3; + } + + #player-bar { + flex-wrap: wrap; + } + + #now-playing { + flex: 1 1 180px; + } + + #player-controls { + flex: 999 1 420px; + } + + #volume-controls { + flex: 0 1 180px; + margin-left: auto; + } +} + /* Mobile responsive styles */ @media (max-width: 768px) { html, body { @@ -514,6 +554,7 @@ button:hover { background: #333; } } #now-playing { width: 100%; + flex: 0 0 auto; display: flex; align-items: center; gap: 0.5rem; @@ -523,7 +564,7 @@ button:hover { background: #333; } flex: 1; font-size: 0.85rem; } - #player-controls { width: 100%; } + #player-controls { width: 100%; flex: 0 0 auto; } #progress-row { display: grid; grid-template-columns: minmax(42px, auto) 44px 44px 44px minmax(58px, auto); @@ -581,9 +622,11 @@ button:hover { background: #333; } padding: 0.2rem 0.4rem; } #volume-controls { + flex: 0 0 auto; justify-content: center; gap: 0.3rem; width: 100%; + margin-left: 0; } #btn-stream-only { display: none; } #btn-mute { diff --git a/public/trackContainer.js b/public/trackContainer.js index f4a6b98..65719a5 100644 --- a/public/trackContainer.js +++ b/public/trackContainer.js @@ -888,10 +888,10 @@ // Adjust if off-screen const rect = menu.getBoundingClientRect(); if (rect.right > window.innerWidth) { - menu.style.left = (window.innerWidth - rect.width - 5) + "px"; + menu.style.left = Math.max(5, window.innerWidth - rect.width - 5) + "px"; } if (rect.bottom > window.innerHeight) { - menu.style.top = (window.innerHeight - rect.height - 5) + "px"; + menu.style.top = Math.max(5, window.innerHeight - rect.height - 5) + "px"; } activeContextMenu = menu; diff --git a/public/ui.js b/public/ui.js index 0cd90dd..80bbd70 100644 --- a/public/ui.js +++ b/public/ui.js @@ -186,7 +186,7 @@ }; // Restore last active tab - const savedTab = localStorage.getItem("blastoise_mobile_tab") || "channels-panel"; + const savedTab = localStorage.getItem("blastoise_mobile_tab") || "queue-panel"; function setActiveTab(panelId) { tabs.forEach(t => t.classList.toggle("active", t.dataset.panel === panelId));