125 lines
4.7 KiB
JavaScript
125 lines
4.7 KiB
JavaScript
/*
|
||
* 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;
|
||
}
|
||
}
|