blastoise-archive/public/ui.js

164 lines
5.9 KiB
JavaScript

// MusicRoom - UI module
// Progress bar, buffer display, and UI state updates
(function() {
const M = window.MusicRoom;
// Create buffer segments on load
for (let i = 0; i < M.SEGMENTS; i++) {
const seg = document.createElement("div");
seg.className = "segment";
M.$("#buffer-bar").appendChild(seg);
}
// Update general UI state
M.updateUI = function() {
const isPlaying = M.synced ? !M.serverPaused : !M.audio.paused;
M.$("#btn-sync").classList.toggle("synced", M.wantSync);
M.$("#btn-sync").classList.toggle("connected", M.synced);
M.$("#btn-sync").title = M.wantSync ? "Unsync" : "Sync";
M.$("#status").textContent = M.synced ? "Synced" : (M.wantSync ? "Connecting..." : "Local");
M.$("#sync-indicator").classList.toggle("visible", M.synced);
M.$("#progress-bar").classList.toggle("synced", M.synced);
M.$("#progress-bar").classList.toggle("local", !M.synced);
M.$("#progress-bar").classList.toggle("muted", M.audio.volume === 0);
M.$("#btn-mute").textContent = M.audio.volume === 0 ? "🔇" : "🔊";
M.$("#status-icon").textContent = isPlaying ? "⏸" : "▶";
// Show/hide controls based on permissions
const hasControl = M.canControl();
M.$("#status-icon").style.cursor = hasControl || !M.synced ? "pointer" : "default";
};
// Update auth-related UI
M.updateAuthUI = function() {
if (M.currentUser) {
M.$("#login-panel").classList.add("hidden");
M.$("#player-content").classList.add("visible");
if (M.currentUser.isGuest) {
M.$("#current-username").textContent = "Guest";
M.$("#btn-logout").textContent = "Sign In";
} else {
M.$("#current-username").textContent = M.currentUser.username;
M.$("#btn-logout").textContent = "Logout";
}
M.$("#admin-badge").style.display = M.currentUser.isAdmin ? "inline" : "none";
} else {
M.$("#login-panel").classList.remove("hidden");
M.$("#player-content").classList.remove("visible");
// Pause and unsync when login panel is shown
if (!M.audio.paused) {
M.localTimestamp = M.audio.currentTime;
M.audio.pause();
}
if (M.synced && M.ws) {
M.synced = false;
M.ws.close();
M.ws = null;
}
// Show guest button if server allows guests
if (M.serverStatus?.allowGuests) {
M.$("#guest-section").classList.remove("hidden");
} else {
M.$("#guest-section").classList.add("hidden");
}
}
M.updateUI();
};
// Progress bar and buffer update loop (250ms interval)
setInterval(() => {
if (M.serverTrackDuration <= 0) return;
let t, dur;
if (M.synced) {
t = M.audio.paused ? M.getServerTime() : M.audio.currentTime;
dur = M.audio.duration || M.serverTrackDuration;
} else {
t = M.audio.paused ? M.localTimestamp : M.audio.currentTime;
dur = M.audio.duration || M.serverTrackDuration;
}
const pct = Math.min((t / dur) * 100, 100);
if (Math.abs(pct - M.lastProgressPct) > 0.1) {
M.$("#progress-bar").style.width = pct + "%";
M.lastProgressPct = pct;
}
const timeCurrent = M.fmt(t);
const timeTotal = M.fmt(dur);
if (timeCurrent !== M.lastTimeCurrent) {
M.$("#time-current").textContent = timeCurrent;
M.lastTimeCurrent = timeCurrent;
}
if (timeTotal !== M.lastTimeTotal) {
M.$("#time-total").textContent = timeTotal;
M.lastTimeTotal = timeTotal;
}
// Update buffer segments
const segments = M.$("#buffer-bar").children;
const segmentDur = dur / M.SEGMENTS;
let availableCount = 0;
const trackCache = M.getTrackCache(M.currentTrackId);
for (let i = 0; i < M.SEGMENTS; i++) {
const segStart = i * segmentDur;
const segEnd = (i + 1) * segmentDur;
let available = trackCache.has(i); // Check our cache first
if (!available) {
// Check browser's native buffer
for (let j = 0; j < M.audio.buffered.length; j++) {
const bufStart = M.audio.buffered.start(j);
const bufEnd = M.audio.buffered.end(j);
if (bufStart <= segStart && bufEnd >= segEnd) {
available = true;
// Sync browser buffer to our trackCache
trackCache.add(i);
break;
}
}
}
if (available) availableCount++;
const isAvailable = segments[i].classList.contains("available");
const isLoading = segments[i].classList.contains("loading");
const shouldBeLoading = !available && M.loadingSegments.has(i);
if (available !== isAvailable) segments[i].classList.toggle("available", available);
if (shouldBeLoading !== isLoading) segments[i].classList.toggle("loading", shouldBeLoading);
}
// Check if all segments now cached - trigger full cache
if (trackCache.size >= M.SEGMENTS && !M.cachedTracks.has(M.currentTrackId)) {
M.checkAndCacheComplete(M.currentTrackId);
}
// Update download speed display
const kbps = M.downloadSpeed > 0 ? M.downloadSpeed * 8 / 1000 : 0;
const bufferPct = Math.round(availableCount / M.SEGMENTS * 100);
let speedText = "";
if (kbps > 0) {
speedText = kbps >= 1024 ? ` | ${(kbps / 1024).toFixed(1)} Mb/s` : ` | ${Math.round(kbps)} kb/s`;
}
if (bufferPct !== M.lastBufferPct || speedText !== M.lastSpeedText) {
M.$("#download-speed").textContent = `${bufferPct}% buffered${speedText}`;
M.lastBufferPct = bufferPct;
M.lastSpeedText = speedText;
}
}, 250);
// Prefetch loop (1s interval)
setInterval(() => {
if (M.currentTrackId && M.audio.src) {
M.prefetchSegments();
}
}, 1000);
// Cache status check (5s interval) - updates indicators when tracks finish caching
let lastCacheSize = 0;
setInterval(async () => {
const currentSize = M.cachedTracks.size;
if (currentSize !== lastCacheSize) {
lastCacheSize = currentSize;
M.renderQueue();
M.renderLibrary();
}
}, 5000);
})();