Improve responsive queue layout and remember channels

This commit is contained in:
peterino2 2026-07-02 13:19:41 -07:00
parent 6a62242482
commit aef6b9590e
7 changed files with 134 additions and 19 deletions

View File

@ -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;
}

View File

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

View File

@ -53,8 +53,8 @@
<!-- Mobile tab bar -->
<div id="mobile-tabs">
<button class="mobile-tab active" data-panel="channels-panel">Channels</button>
<button class="mobile-tab" data-panel="library-panel">Library</button>
<button class="mobile-tab" data-panel="queue-panel">Queue</button>
<button class="mobile-tab" data-panel="library-panel">Library</button>
</div>
<div id="main-content">

View File

@ -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" });
}
}
};

View File

@ -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 {

View File

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

View File

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