ok
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user