Web frontend
This commit is contained in:
230
web/src/components/PlayView.tsx
Normal file
230
web/src/components/PlayView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user