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

View File

@@ -0,0 +1,230 @@
/** The full-screen now-playing view. */
import type { Album, PlayerState } from "../api/types";
import { albumLine, isBook } from "../lib/covers";
import { clock, remainingInAlbum } from "../lib/format";
import { Cover } from "./Cover";
import { ProgressBar } from "./ProgressBar";
import { Transport } from "./Transport";
import { VolumeBars } from "./VolumeBars";
interface Props {
state: PlayerState;
album: Album | null;
position: number;
onToggle: () => void;
onNext: () => void;
onPrevious: () => void;
onSeek: (position: number) => void;
onVolume: (percent: number) => void;
onMute: () => void;
onBrowse: () => void;
}
export function PlayView({
state,
album,
position,
onToggle,
onNext,
onPrevious,
onSeek,
onVolume,
onMute,
onBrowse,
}: Props) {
const book = album ? isBook(album) : false;
const remaining = album
? remainingInAlbum(
album.tracks.map((track) => track.duration),
state.track_index,
position,
)
: 0;
return (
<div style={{ position: "relative", zIndex: 1, height: "100%", minHeight: 0, overflow: "auto" }}>
<div
style={{
minHeight: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "clamp(8px, 1.8vh, 26px)",
padding: "clamp(14px, 3.5vh, 40px) 40px",
}}
>
<img
src="/dolphin-mascot.png"
alt=""
style={{
position: "absolute",
top: "14%",
left: 0,
width: 140,
height: 140,
objectFit: "contain",
zIndex: 0,
pointerEvents: "none",
opacity: 0.92,
animation: "dolphinSwim 62s linear infinite",
filter: "drop-shadow(0 10px 26px oklch(10% 0.05 210 / .45))",
}}
/>
<div
style={{
height: "min(340px, 32vh)",
flex: "none",
position: "relative",
zIndex: 1,
display: "flex",
filter: "drop-shadow(0 20px 40px oklch(10% 0.05 210 / .55))",
}}
>
{album ? (
<Cover
album={album}
size={340}
fit="height"
radius={book ? "18px 34px 34px 18px" : "28px"}
label
/>
) : (
<div
style={{
height: "100%",
aspectRatio: 1,
borderRadius: 28,
background: "oklch(35% 0.03 210)",
}}
/>
)}
</div>
<div style={{ textAlign: "center", maxWidth: 760, position: "relative", zIndex: 1 }}>
<div
style={{
fontSize: "clamp(28px, 4.6vh, 46px)",
fontWeight: 900,
color: "var(--paper)",
lineHeight: 1.1,
textWrap: "pretty",
}}
>
{state.track_title ?? "Wähl ein Album!"}
</div>
<div
style={{
fontSize: 20,
fontWeight: 700,
color: "oklch(88% 0.02 210 / .8)",
marginTop: 8,
}}
>
{album ? albumLine(album) : "Tippen oder klicken"}
</div>
{album && (
<div
style={{
fontSize: 15,
fontWeight: 800,
color: "var(--accent-dim)",
marginTop: 6,
}}
>
{book ? "Kapitel" : "Song"} {state.track_index + 1} von {state.track_count}
</div>
)}
</div>
<div style={{ width: "min(680px, 90%)", position: "relative", zIndex: 1 }}>
<ProgressBar
position={position}
duration={state.duration}
onSeek={onSeek}
height={14}
interactive
/>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
marginTop: 8,
fontSize: 14,
fontWeight: 800,
color: "oklch(88% 0.02 210 / .8)",
}}
>
<span style={{ fontFamily: "ui-monospace, Menlo, monospace" }}>
{state.duration ? `${clock(state.duration - position)}` : ""}
</span>
<span>
{album ? `Noch ${clock(remaining)} ${book ? "im Hörbuch" : "im Album"}` : ""}
</span>
</div>
</div>
<div style={{ position: "relative", zIndex: 1 }}>
<Transport
playing={state.playing}
onToggle={onToggle}
onNext={onNext}
onPrevious={onPrevious}
size={72}
gap={22}
/>
</div>
<div style={{ position: "relative", zIndex: 1 }}>
<VolumeBars
volume={state.volume}
onChange={onVolume}
onMute={onMute}
barWidth={18}
barHeight="clamp(26px, 4.6vh, 44px)"
gap={5}
iconSize={24}
/>
</div>
<button
onClick={onBrowse}
style={{
position: "absolute",
top: 28,
left: 32,
border: "none",
cursor: "pointer",
background: "oklch(97% 0.01 210 / .95)",
borderRadius: 999,
padding: "11px 20px",
fontSize: 14,
fontWeight: 800,
color: "var(--ink)",
display: "flex",
alignItems: "center",
gap: 8,
boxShadow: "0 8px 24px oklch(15% 0.05 210 / .4)",
}}
>
<span
style={{
font: "800 13px ui-monospace, monospace",
background: "var(--ink)",
color: "#fff",
padding: "2px 8px",
borderRadius: 6,
}}
>
/
</span>
Zurück zur Suche
</button>
</div>
</div>
);
}