Web frontend

This commit is contained in:
2026-08-27 12:32:20 +02:00
parent d44c24ec97
commit edb6e5e027
97 changed files with 9535 additions and 195 deletions

43
web/src/api/client.ts Normal file
View File

@@ -0,0 +1,43 @@
/** Every call the UI makes. Commands are fire-and-forget: the websocket reports back. */
import type { Album, PlayerState, Settings } from "./types";
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`/api${path}`, {
headers: init?.body ? { "Content-Type": "application/json" } : undefined,
...init,
});
if (!response.ok) {
throw new Error(`${init?.method ?? "GET"} ${path} failed: ${response.status}`);
}
return response.status === 204 ? (undefined as T) : ((await response.json()) as T);
}
function post(path: string, body?: unknown): Promise<void> {
return request<void>(path, {
method: "POST",
body: body === undefined ? undefined : JSON.stringify(body),
});
}
export const api = {
library: () => request<{ albums: Album[] }>("/library").then((body) => body.albums),
state: () => request<PlayerState>("/state"),
refreshLibrary: () => post("/library/refresh"),
play: (albumId: string, trackIndex = 0) =>
post("/play", { album_id: albumId, track_index: trackIndex }),
resume: () => post("/resume"),
pause: () => post("/pause"),
next: () => post("/next"),
previous: () => post("/previous"),
seek: (position: number) => post("/seek", { position }),
setVolume: (percent: number) => post("/volume", { percent }),
nudgeVolume: (deltaPercent: number) => post("/volume", { delta_percent: deltaPercent }),
settings: () => request<Settings>("/settings"),
saveSettings: (settings: Settings) =>
request<Settings>("/settings", { method: "PUT", body: JSON.stringify(settings) }),
};
export const coverUrl = (albumId: string) => `/api/albums/${albumId}/cover`;