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));