Improve responsive queue layout and remember channels
This commit is contained in:
parent
ac740a46bb
commit
dcbb0a36e9
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in New Issue