ok
This commit is contained in:
+39
-14
@@ -1,7 +1,8 @@
|
||||
/*
|
||||
* Browser client for the web player.
|
||||
*
|
||||
* refresh and command obtain complete player-state snapshots from the server.
|
||||
* PlayerStateClient merges status updates with the cached playlist; tracks are
|
||||
* transferred only when their version changes. State requests share a queue.
|
||||
* render stores that snapshot and passes it to the selector, library, playlist,
|
||||
* and playback renderers. Collection renderers retain a DOM signature so the
|
||||
* one-second polling loop only rebuilds lists whose contents changed.
|
||||
@@ -11,6 +12,9 @@
|
||||
* returned state has been rendered.
|
||||
*/
|
||||
|
||||
import { PlayerStateClient } from "./player-state.js";
|
||||
import { PlaylistLibrary } from "./playlist-library.js";
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Browser elements and state
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
@@ -44,6 +48,7 @@ const elements = {
|
||||
tabAdd: document.querySelector("#tab-add"),
|
||||
count: document.querySelector("#track-count"),
|
||||
playlistClear: document.querySelector("#playlist-clear"),
|
||||
playlistSave: document.querySelector("#playlist-save"),
|
||||
playlistEmpty: document.querySelector("#playlist-empty"),
|
||||
playlist: document.querySelector("#playlist"),
|
||||
bits: document.querySelector("#audio-bits"),
|
||||
@@ -67,6 +72,9 @@ let state = null;
|
||||
let seekBusy = false;
|
||||
let draggedTrack = null;
|
||||
let commandBusy = false;
|
||||
let refreshBusy = false;
|
||||
const playerState = new PlayerStateClient(api);
|
||||
const playlistLibrary = new PlaylistLibrary(document.querySelector("#library-pane"), command, formatPlayingTime);
|
||||
|
||||
// Represents an unsuccessful API response, including its HTTP status and code.
|
||||
class ApiError extends Error {
|
||||
@@ -193,7 +201,7 @@ async function command(name, data = {}, pendingMessage = "") {
|
||||
|
||||
commandBusy = true;
|
||||
try {
|
||||
render(await api(`/api/command/${name}`, data));
|
||||
render(await playerState.request(`/api/command/${name}`, data));
|
||||
} catch (error) {
|
||||
setStatus(errorMessage(error));
|
||||
} finally {
|
||||
@@ -376,6 +384,14 @@ function renameTab(tab) {
|
||||
}
|
||||
}
|
||||
|
||||
// Give this tab a library name while retaining its identity and current tracks.
|
||||
function savePlaylist() {
|
||||
if (!state) return;
|
||||
const tab = state.tabs[state.currentTab];
|
||||
const name = window.prompt(t("playlistName"), tab.name === "Default" ? t("defaultPlaylist") : tab.name);
|
||||
if (name !== null) command("playlist-save", { id: tab.id, name });
|
||||
}
|
||||
|
||||
// Builds one playlist tab, including its count and optional delete control.
|
||||
function createTab(tab, canDelete) {
|
||||
const button = document.createElement("button");
|
||||
@@ -396,11 +412,11 @@ function createTab(tab, canDelete) {
|
||||
button.addEventListener("click", () => command("tab-select", { index: tab.index }));
|
||||
button.addEventListener("dblclick", () => renameTab(tab));
|
||||
|
||||
if (canDelete) {
|
||||
if (canDelete || tab.saved) {
|
||||
const remove = document.createElement("span");
|
||||
remove.className = "tab-delete";
|
||||
remove.textContent = "×";
|
||||
remove.title = t("removePlaylist");
|
||||
remove.title = t(tab.saved ? "closePlaylist" : "removePlaylist");
|
||||
remove.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
command("tab-delete", { index: tab.index });
|
||||
@@ -414,7 +430,7 @@ function createTab(tab, canDelete) {
|
||||
// Rebuilds changed tabs and marks the server-selected tab as active.
|
||||
function renderTabs(nextState) {
|
||||
const signature = nextState.tabs
|
||||
.map((tab) => `${tab.index}:${tab.name}:${tab.count}`)
|
||||
.map((tab) => `${tab.index}:${tab.name}:${tab.count}:${tab.saved}`)
|
||||
.join("|");
|
||||
if (elements.tabs.dataset.signature !== signature) {
|
||||
const canDelete = nextState.tabs.length > 1;
|
||||
@@ -510,17 +526,19 @@ function createPlaylistRow(track) {
|
||||
|
||||
// Rebuilds changed tracks and updates the active row and playlist summary.
|
||||
function renderPlaylist(nextState) {
|
||||
const signature = nextState.tracks
|
||||
.map((track) => `${track.index}:${track.title}:${track.artist}:${track.album}:${track.duration}`)
|
||||
.join("|");
|
||||
const signature = nextState.playlistVersion;
|
||||
if (elements.playlist.dataset.signature !== signature) {
|
||||
const rows = nextState.tracks.map(createPlaylistRow);
|
||||
elements.playlist.replaceChildren(...rows);
|
||||
elements.playlist.dataset.signature = signature;
|
||||
}
|
||||
|
||||
for (const row of elements.playlist.children) {
|
||||
row.classList.toggle("current", Number(row.dataset.index) === nextState.currentIndex);
|
||||
const previousRow = elements.playlist.querySelector(".current");
|
||||
const currentRow = Number.isInteger(nextState.currentIndex)
|
||||
? elements.playlist.children[nextState.currentIndex] : null;
|
||||
if (previousRow !== currentRow) {
|
||||
previousRow?.classList.remove("current");
|
||||
currentRow?.classList.add("current");
|
||||
}
|
||||
|
||||
elements.playlistEmpty.hidden = nextState.tracks.length > 0;
|
||||
@@ -528,6 +546,8 @@ function renderPlaylist(nextState) {
|
||||
const tab = nextState.tabs[nextState.currentTab];
|
||||
elements.count.textContent = `${nextState.tracks.length} ${t(trackLabel)}, ${formatPlayingTime(tab.duration)}`;
|
||||
elements.playlistClear.disabled = nextState.tracks.length === 0;
|
||||
elements.playlistSave.disabled = tab.saved;
|
||||
elements.playlistSave.textContent = t(tab.saved ? "playlistSaved" : "savePlaylist");
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
@@ -645,6 +665,7 @@ function render(nextState) {
|
||||
state = nextState;
|
||||
renderSelectors(nextState);
|
||||
renderBrowser(nextState);
|
||||
playlistLibrary.render(nextState.savedPlaylists);
|
||||
renderTabs(nextState);
|
||||
renderPlaylist(nextState);
|
||||
renderPlayer(nextState);
|
||||
@@ -726,7 +747,7 @@ function handleTranslationChange() {
|
||||
// Starts renderer discovery and renders the state returned by the server.
|
||||
async function discoverRenderers() {
|
||||
try {
|
||||
render(await api("/api/discover", {}));
|
||||
render(await playerState.request("/api/discover", {}));
|
||||
} catch (error) {
|
||||
setStatus(errorMessage(error));
|
||||
}
|
||||
@@ -768,14 +789,15 @@ function seek() {
|
||||
command("seek", { percentage: Number(elements.seek.value) });
|
||||
}
|
||||
|
||||
// Polls a complete state snapshot unless a browser command is still active.
|
||||
// Polls status and changed tracks, without overlapping polls or busy commands.
|
||||
async function refresh() {
|
||||
if (commandBusy) {
|
||||
if (commandBusy || refreshBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshBusy = true;
|
||||
try {
|
||||
render(await api("/api/state"));
|
||||
render(await playerState.request("/api/state"));
|
||||
hideLogin();
|
||||
} catch (error) {
|
||||
if (error.code === "authentication-required") {
|
||||
@@ -783,6 +805,8 @@ async function refresh() {
|
||||
} else {
|
||||
setStatus(t("noConnection", { message: errorMessage(error) }));
|
||||
}
|
||||
} finally {
|
||||
refreshBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,6 +883,7 @@ elements.library.addEventListener("change", () => {
|
||||
elements.libraryUp.addEventListener("click", () => command("up"));
|
||||
elements.tabAdd.addEventListener("click", () => command("tab-add"));
|
||||
elements.playlistClear.addEventListener("click", () => command("playlist-clear"));
|
||||
elements.playlistSave.addEventListener("click", savePlaylist);
|
||||
elements.loginForm.addEventListener("submit", login);
|
||||
elements.logout.addEventListener("click", logout);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user