Files
rkt-web-player/public/playlist-library.js
T
2026-09-08 15:20:27 +02:00

125 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}
}