// MusicRoom - Utilities module
// DOM helpers, formatting, toast notifications
(function() {
const M = window.MusicRoom;
// DOM selector helper
M.$ = (s) => document.querySelector(s);
// Format seconds as m:ss
M.fmt = function(sec) {
if (!sec || !isFinite(sec)) return "0:00";
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return m + ":" + String(s).padStart(2, "0");
};
// Toast history
M.toastHistory = [];
// Toast notifications (log style - multiple visible)
M.showToast = function(message, type = "info", duration = 5000) {
const container = M.$("#toast-container");
const toast = document.createElement("div");
toast.className = "toast toast-" + type;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add("fade-out");
setTimeout(() => toast.remove(), 300);
}, duration);
// Add to history
M.toastHistory.push({
message,
type,
time: new Date()
});
M.updateToastHistory();
};
// Update toast history panel
M.updateToastHistory = function() {
const list = M.$("#toast-history-list");
if (!list) return;
list.innerHTML = "";
// Show newest first
const items = [...M.toastHistory].reverse().slice(0, 50);
for (const item of items) {
const div = document.createElement("div");
div.className = "history-item history-" + item.type;
const time = item.time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
div.innerHTML = `${time} ${item.message}`;
list.appendChild(div);
}
};
// Toggle toast history panel
M.toggleToastHistory = function() {
const panel = M.$("#toast-history");
if (panel) {
panel.classList.toggle("hidden");
if (!panel.classList.contains("hidden")) {
M.updateToastHistory();
}
}
};
// Flash permission denied animation
M.flashPermissionDenied = function() {
const row = M.$("#progress-row");
row.classList.remove("denied");
void row.offsetWidth; // Trigger reflow to restart animation
row.classList.add("denied");
setTimeout(() => row.classList.remove("denied"), 500);
};
// Set track title (UI and document title)
M.setTrackTitle = function(title) {
M.currentTitle = title;
const containerEl = M.$("#track-name");
const marqueeEl = containerEl?.querySelector(".marquee-inner");
if (!containerEl || !marqueeEl) return;
document.title = title ? `${title} - MusicRoom` : "MusicRoom";
// First set simple content to measure
marqueeEl.innerHTML = `${title}`;
// Check if title overflows and needs scrolling
requestAnimationFrame(() => {
const titleEl = M.$("#track-title");
const needsScroll = titleEl && titleEl.scrollWidth > containerEl.clientWidth;
containerEl.classList.toggle("scrolling", needsScroll);
// Duplicate text for seamless wrap-around scrolling
if (needsScroll) {
marqueeEl.innerHTML = `${title} • ${title} • `;
}
});
};
// Get current server time (extrapolated)
M.getServerTime = function() {
if (M.serverPaused) return M.serverTimestamp;
return M.serverTimestamp + (Date.now() - M.lastServerUpdate) / 1000;
};
// Check if current user can control playback
M.canControl = function() {
if (!M.currentUser) return false;
if (M.currentUser.isAdmin) return true;
return M.currentUser.permissions?.some(p =>
p.resource_type === "channel" &&
(p.resource_id === M.currentChannelId || p.resource_id === null) &&
p.permission === "control"
);
};
})();