multi player / per user playing and translation

This commit is contained in:
2026-08-28 16:53:05 +02:00
parent 2743dfb4c0
commit e10f2cf7d7
17 changed files with 1432 additions and 482 deletions
+80 -26
View File
@@ -1,5 +1,6 @@
const elements = {
renderer: document.querySelector("#renderer"),
language: document.querySelector("#language"),
discover: document.querySelector("#discover"),
previous: document.querySelector("#previous"),
play: document.querySelector("#play"),
@@ -9,6 +10,8 @@ const elements = {
position: document.querySelector("#position"),
duration: document.querySelector("#duration"),
repeat: document.querySelector("#repeat"),
volumeControl: document.querySelector("#volume-control"),
volumeToggle: document.querySelector("#volume-toggle"),
volume: document.querySelector("#volume"),
volumeValue: document.querySelector("#volume-value"),
library: document.querySelector("#library"),
@@ -41,6 +44,8 @@ const elements = {
loginSubmit: document.querySelector("#login-submit"),
};
const { t } = window.RktTranslate;
let state = null;
let seekBusy = false;
let draggedTrack = null;
@@ -102,9 +107,16 @@ async function refreshAuth() {
try {
const auth = await api("/api/auth/status");
elements.logout.hidden = !auth.enabled || !auth.authenticated;
if (auth.enabled && !auth.authenticated) showLogin();
if (auth.enabled && !auth.authenticated) {
showLogin();
} else {
const preferences = await api("/api/preferences");
if (window.RktTranslate.supported.includes(preferences.language)) {
window.RktTranslate.setLanguage(preferences.language);
}
}
} catch (error) {
setStatus(`Authenticatiestatus onbekend: ${error.message}`);
setStatus(t("authUnknown", { message: error.message }));
}
}
@@ -136,7 +148,7 @@ function replaceSelect(select, items, selectedId, signature) {
function renderSelectors(nextState) {
const rendererItems = nextState.renderers.map((item) => ({
id: item.id,
label: `${item.name} · ${item.kind.toUpperCase()}`,
label: `${item.name} · ${item.kind === "local" ? t("rendererLocal") : item.kind.toUpperCase()}`,
}));
replaceSelect(
elements.renderer,
@@ -158,6 +170,7 @@ function renderSelectors(nextState) {
elements.discover.classList.toggle("busy", nextState.discovering);
elements.discover.disabled = nextState.discovering;
elements.renderer.disabled = nextState.renderers.length === 0;
elements.library.disabled = nextState.libraries.length === 0;
}
@@ -178,7 +191,7 @@ function entryAction(label, title, handler) {
function renderBrowser(nextState) {
const selectedLibrary = nextState.libraries.find((item) => item.id === nextState.libraryId);
const path = [selectedLibrary?.name, ...nextState.browser.path].filter(Boolean);
elements.breadcrumb.textContent = path.length ? path.join(" / ") : "Geen bibliotheek";
elements.breadcrumb.textContent = path.length ? path.join(" / ") : t("noLibrary");
elements.breadcrumb.title = elements.breadcrumb.textContent;
elements.libraryUp.disabled = !nextState.browser.canGoUp;
elements.libraryEmpty.hidden = nextState.libraries.length > 0;
@@ -205,11 +218,11 @@ function renderBrowser(nextState) {
const actions = document.createElement("span");
actions.className = "entry-actions";
actions.append(
entryAction("▶", `${entry.name} nu afspelen`, () => {
command("item-play", { index: entry.index }, `${entry.name} laden…`);
entryAction("▶", t("playNow", { name: entry.name }), () => {
command("item-play", { index: entry.index }, t("loadingNamed", { name: entry.name }));
}),
entryAction("", `${entry.name} toevoegen`, () => {
command("item-add", { index: entry.index }, `${entry.name} toevoegen…`);
entryAction("", t("addNamed", { name: entry.name }), () => {
command("item-add", { index: entry.index }, t("addingNamed", { name: entry.name }));
}),
);
@@ -218,7 +231,7 @@ function renderBrowser(nextState) {
row.addEventListener("click", () => command("browse", { index: entry.index }));
} else {
row.addEventListener("dblclick", () => {
command("item-play", { index: entry.index }, `${entry.name} laden…`);
command("item-play", { index: entry.index }, t("loadingNamed", { name: entry.name }));
});
}
row.addEventListener("keydown", (event) => {
@@ -226,10 +239,10 @@ function renderBrowser(nextState) {
command(
entry.kind === "container" ? "browse" : "item-play",
{ index: entry.index },
entry.kind === "track" ? `${entry.name} laden…` : "",
entry.kind === "track" ? t("loadingNamed", { name: entry.name }) : "",
);
} else if (event.key === "+") {
command("item-add", { index: entry.index }, `${entry.name} toevoegen…`);
command("item-add", { index: entry.index }, t("addingNamed", { name: entry.name }));
}
});
return row;
@@ -253,7 +266,7 @@ function renderTabs(nextState) {
const name = document.createElement("span");
name.className = "tab-name";
name.textContent = tab.name;
name.textContent = tab.name === "Default" ? t("defaultPlaylist") : tab.name;
const count = document.createElement("span");
count.className = "tab-count";
count.textContent = tab.count;
@@ -261,7 +274,7 @@ function renderTabs(nextState) {
button.addEventListener("click", () => command("tab-select", { index: tab.index }));
button.addEventListener("dblclick", () => {
const newName = window.prompt("Naam van de afspeellijst", tab.name);
const newName = window.prompt(t("playlistName"), tab.name);
if (newName !== null) command("tab-rename", { index: tab.index, name: newName });
});
@@ -269,7 +282,7 @@ function renderTabs(nextState) {
const remove = document.createElement("span");
remove.className = "tab-delete";
remove.textContent = "×";
remove.title = "Afspeellijst verwijderen";
remove.title = t("removePlaylist");
remove.addEventListener("click", (event) => {
event.stopPropagation();
command("tab-delete", { index: tab.index });
@@ -328,8 +341,8 @@ function renderPlaylist(nextState) {
remove.className = "row-action";
remove.type = "button";
remove.textContent = "×";
remove.title = `${track.title} verwijderen`;
remove.setAttribute("aria-label", `${track.title} verwijderen`);
remove.title = t("removeNamed", { name: track.title });
remove.setAttribute("aria-label", t("removeNamed", { name: track.title }));
remove.addEventListener("click", (event) => {
event.stopPropagation();
command("track-remove", { index: track.index });
@@ -366,7 +379,7 @@ function renderPlaylist(nextState) {
}
elements.playlistEmpty.hidden = nextState.tracks.length > 0;
elements.count.textContent = `${nextState.tracks.length} ${nextState.tracks.length === 1 ? "track" : "tracks"}`;
elements.count.textContent = `${nextState.tracks.length} ${t(nextState.tracks.length === 1 ? "oneTrack" : "manyTracks")}`;
elements.playlistClear.disabled = nextState.tracks.length === 0;
}
@@ -374,10 +387,10 @@ function renderPlayer(nextState) {
const current = Number.isInteger(nextState.currentIndex)
? nextState.tracks[nextState.currentIndex]
: null;
elements.title.textContent = current ? current.title : "Nog niets gekozen";
elements.title.textContent = current ? current.title : t("nothingSelected");
elements.meta.textContent = current
? [current.artist, current.album].filter(Boolean).join(" · ") || current.source
: "Voeg een track toe vanuit de bibliotheek";
: t("addTrackHint");
const artworkId = current?.artworkId || "";
if (elements.coverImage.dataset.artworkId !== artworkId) {
@@ -395,8 +408,8 @@ function renderPlayer(nextState) {
const playing = nextState.state === "playing" || nextState.state === "starting";
elements.play.textContent = playing ? "Ⅱ" : "▶";
elements.play.setAttribute("aria-label", playing ? "Pauzeren" : "Afspelen");
elements.play.disabled = nextState.tracks.length === 0;
elements.play.setAttribute("aria-label", t(playing ? "pause" : "play"));
elements.play.disabled = nextState.tracks.length === 0 || nextState.renderers.length === 0;
elements.previous.disabled = nextState.tracks.length === 0;
elements.next.disabled = nextState.tracks.length === 0;
elements.stop.disabled = nextState.state === "stopped";
@@ -412,14 +425,20 @@ function renderPlayer(nextState) {
elements.volume.value = nextState.volume;
elements.volumeValue.value = `${Math.round(nextState.volume)}%`;
elements.volumeToggle.setAttribute(
"aria-label",
t("volumePercent", { value: Math.round(nextState.volume) }),
);
elements.repeat.dataset.repeat = nextState.repeat;
elements.repeat.classList.toggle("active", nextState.repeat !== "off");
const repeatNames = { off: "Herhalen uit", all: "Alles herhalen", one: "Eén track herhalen" };
const repeatNames = { off: t("repeatOff"), all: t("repeatAll"), one: t("repeatOne") };
elements.repeat.title = repeatNames[nextState.repeat] || repeatNames.off;
elements.bits.textContent = nextState.bits ? `${nextState.bits} bit` : "— bit";
elements.rate.textContent = nextState.rate ? `${(nextState.rate / 1000).toFixed(1)} kHz` : "— kHz";
elements.channels.textContent = nextState.channels ? `${nextState.channels} kanalen` : "— kanalen";
elements.channels.textContent = nextState.channels
? `${nextState.channels} ${t(nextState.channels === 1 ? "oneChannel" : "channels")}`
: `${t("channels")}`;
elements.format.textContent = nextState.format || "—";
elements.source.textContent = nextState.source || "—";
}
@@ -441,7 +460,7 @@ function render(nextState) {
renderTabs(nextState);
renderPlaylist(nextState);
renderPlayer(nextState);
setStatus(nextState.error || (nextState.discovering ? "Netwerkspelers zoeken…" : ""));
setStatus(nextState.error || (nextState.discovering ? t("searchingPlayers") : ""));
}
elements.play.addEventListener("click", () => {
@@ -458,6 +477,22 @@ elements.repeat.addEventListener("click", () => {
command("repeat", { mode: next });
});
elements.renderer.addEventListener("change", () => command("renderer", { id: elements.renderer.value }));
elements.language.value = window.RktTranslate.language();
elements.language.addEventListener("change", async () => {
window.RktTranslate.setLanguage(elements.language.value);
try {
await api("/api/preferences", { language: elements.language.value });
} catch (error) {
setStatus(error.message);
}
});
window.addEventListener("rkt-language-change", () => {
elements.language.value = window.RktTranslate.language();
for (const element of [elements.renderer, elements.libraryEntries, elements.tabs, elements.playlist]) {
delete element.dataset.signature;
}
if (state) render(state);
});
elements.discover.addEventListener("click", async () => {
try {
render(await api("/api/discover", {}));
@@ -465,10 +500,29 @@ elements.discover.addEventListener("click", async () => {
setStatus(error.message);
}
});
elements.volumeToggle.addEventListener("click", () => {
const open = !elements.volumeControl.classList.contains("open");
elements.volumeControl.classList.toggle("open", open);
elements.volumeToggle.setAttribute("aria-expanded", String(open));
if (open) elements.volume.focus();
});
elements.volume.addEventListener("input", () => {
elements.volumeValue.value = `${elements.volume.value}%`;
});
elements.volume.addEventListener("change", () => command("volume", { value: Number(elements.volume.value) }));
document.addEventListener("pointerdown", (event) => {
if (!elements.volumeControl.contains(event.target)) {
elements.volumeControl.classList.remove("open");
elements.volumeToggle.setAttribute("aria-expanded", "false");
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && elements.volumeControl.classList.contains("open")) {
elements.volumeControl.classList.remove("open");
elements.volumeToggle.setAttribute("aria-expanded", "false");
elements.volumeToggle.focus();
}
});
elements.seek.addEventListener("pointerdown", () => { seekBusy = true; });
elements.seek.addEventListener("change", () => {
seekBusy = false;
@@ -488,7 +542,7 @@ async function refresh() {
if (error.code === "authentication-required") {
showLogin();
} else {
setStatus(`Geen verbinding: ${error.message}`);
setStatus(t("noConnection", { message: error.message }));
}
}
}
@@ -518,7 +572,7 @@ elements.logout.addEventListener("click", async () => {
await api("/api/auth/logout", {});
} finally {
elements.logout.hidden = true;
showLogin("Je bent uitgelogd.");
showLogin(t("loggedOut"));
}
});