This commit is contained in:
2026-09-08 15:20:27 +02:00
parent 4b9e6c5651
commit 2eb9a9590e
15 changed files with 1023 additions and 127 deletions
+124
View File
@@ -0,0 +1,124 @@
/*
* 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;
}
}