Web frontend
This commit is contained in:
43
web/src/api/client.ts
Normal file
43
web/src/api/client.ts
Normal 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`;
|
||||
65
web/src/api/types.ts
Normal file
65
web/src/api/types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/** The shapes the backend serves. Mirrors `musicmouse/services/web/schemas.py`. */
|
||||
|
||||
export type AlbumKind = "music" | "book";
|
||||
|
||||
export interface TrackAnalysis {
|
||||
tempo: number | null;
|
||||
energy: number | null;
|
||||
valence: number | null;
|
||||
brightness: number | null;
|
||||
beats: boolean;
|
||||
}
|
||||
|
||||
export interface Track {
|
||||
title: string;
|
||||
/** Seconds, read from the file's tags at scan time. */
|
||||
duration: number;
|
||||
analysis: TrackAnalysis | null;
|
||||
}
|
||||
|
||||
export interface Album {
|
||||
id: string;
|
||||
section: string;
|
||||
kind: AlbumKind;
|
||||
title: string;
|
||||
artist: string;
|
||||
series: string | null;
|
||||
figure: string | null;
|
||||
/** Series for audiobooks, artist for music. What the browse view groups by. */
|
||||
category: string;
|
||||
/** Three `#rrggbb`, taken from the cover art or synthesised: primary, secondary, accent. */
|
||||
colors: string[];
|
||||
has_cover: boolean;
|
||||
duration: number;
|
||||
tracks: Track[];
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
playing: boolean;
|
||||
album_id: string | null;
|
||||
album_title: string | null;
|
||||
artist: string | null;
|
||||
kind: AlbumKind | null;
|
||||
track_index: number;
|
||||
track_title: string | null;
|
||||
track_count: number;
|
||||
position: number;
|
||||
duration: number;
|
||||
/** Percent, 0..100. The device's configured range never leaves the backend. */
|
||||
volume: number;
|
||||
active_figure: string | null;
|
||||
connected: { firmware: boolean; mqtt: boolean };
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
min_volume: number;
|
||||
max_volume: number;
|
||||
initial_volume: number;
|
||||
volume_increment: number;
|
||||
button_leds_brightness: number;
|
||||
}
|
||||
|
||||
export type ServerMessage =
|
||||
| { type: "state"; state: PlayerState }
|
||||
| { type: "position"; position: number; duration: number }
|
||||
| { type: "library" };
|
||||
Reference in New Issue
Block a user