/* * Library view for the Folders and Playlists tabs. Saved playlists are server * objects: actions address their UUID and reveal their existing playlist tab. * Only the selected library view and highlighted row are local UI state. */ export class PlaylistLibrary { #tabs; #panels; #list; #empty; #command; #formatDuration; #selectedId = null; #signature = null; // root owns both library panels; command sends server actions and renders state. constructor(root, command, formatDuration) { this.#tabs = [root.querySelector("#folders-tab"), root.querySelector("#saved-playlists-tab")]; this.#panels = [root.querySelector("#folders-panel"), root.querySelector("#saved-playlists-panel")]; this.#list = root.querySelector("#saved-playlists"); this.#empty = root.querySelector("#saved-playlists-empty"); this.#command = command; this.#formatDuration = formatDuration; this.#tabs.forEach((tab, index) => { tab.addEventListener("click", () => this.show(index)); tab.addEventListener("keydown", (event) => { if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) { event.preventDefault(); const next = event.key === "Home" ? 0 : event.key === "End" ? 1 : 1 - index; this.show(next); this.#tabs[next].focus(); } }); }); } // Switch the library panel without changing the selected playlist or playback. show(index) { this.#tabs.forEach((tab, current) => { const active = current === index; tab.classList.toggle("active", active); tab.setAttribute("aria-selected", String(active)); tab.tabIndex = active ? 0 : -1; this.#panels[current].hidden = !active; }); } // Refresh saved playlist summaries only when metadata or language changes. render(playlists) { const signature = JSON.stringify([window.RktTranslate.language(), playlists]); if (signature === this.#signature) return; if (!playlists.some((playlist) => playlist.id === this.#selectedId)) this.#selectedId = null; this.#list.replaceChildren(...playlists.map((playlist) => this.#createEntry(playlist))); this.#empty.hidden = playlists.length > 0; this.#signature = signature; this.#select(this.#selectedId); } #select(id) { this.#selectedId = id; for (const row of this.#list.children) { const selected = row.dataset.id === id; row.classList.toggle("selected", selected); row.setAttribute("aria-current", selected ? "true" : "false"); } } // Each action reveals this playlist by UUID; repeated opening is idempotent. #action(label, title, playlist, play) { const button = document.createElement("button"); button.type = "button"; button.className = "entry-action"; button.textContent = label; button.title = title; button.setAttribute("aria-label", title); button.disabled = play && playlist.count === 0; button.addEventListener("click", (event) => { event.stopPropagation(); this.#select(playlist.id); this.#command(play ? "playlist-play" : "playlist-open", { id: playlist.id }); }); return button; } #createEntry(playlist) { const { t } = window.RktTranslate; const row = document.createElement("li"); row.className = "library-entry"; row.dataset.id = playlist.id; row.tabIndex = 0; const icon = document.createElement("span"); icon.className = "entry-icon"; icon.textContent = "♫"; const name = document.createElement("span"); name.className = "entry-name"; name.textContent = playlist.name; name.title = playlist.name; const details = document.createElement("small"); details.className = "entry-details"; details.textContent = `${playlist.count} ${t(playlist.count === 1 ? "oneTrack" : "manyTracks")}, ${this.#formatDuration(playlist.duration)}`; name.append(details); const actions = document.createElement("span"); actions.className = "entry-actions"; actions.append( this.#action("▶", t("playNow", { name: playlist.name }), playlist, true), this.#action("+", t("openPlaylistNamed", { name: playlist.name }), playlist, false), ); row.append(icon, name, actions); row.addEventListener("click", () => this.#select(playlist.id)); row.addEventListener("dblclick", (event) => { if (event.target.closest("button")) return; this.#command("playlist-open", { id: playlist.id }); }); row.addEventListener("keydown", (event) => { if (event.target !== row) return; if (["Enter", "+", " "].includes(event.key)) { event.preventDefault(); this.#select(playlist.id); if (event.key !== " ") this.#command("playlist-open", { id: playlist.id }); } }); return row; } }