Files
musicmouse/web/src/components/PlayView.tsx

251 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** The full-screen now-playing view. */
import type { Album, PlayerState } from "../api/types";
import { usePlaybackClock } from "../hooks/usePlaybackClock";
import { isBook, nowPlayingText } from "../lib/covers";
import { clock, remainingInAlbum } from "../lib/format";
import { groupOf } from "../lib/search";
import { SHOW_DECORATIVE_ANIMATIONS } from "../lib/theme";
import { Cover } from "./Cover";
import { ProgressBar } from "./ProgressBar";
import { Transport } from "./Transport";
import { VolumeBars } from "./VolumeBars";
interface Props {
state: PlayerState;
album: Album | null;
onToggle: () => void;
onNext: () => void;
onPrevious: () => void;
onSeek: (position: number) => void;
onVolume: (percent: number) => void;
onMute: () => void;
onOpenAlbum: () => void;
/** Assigning the current album/show to a remote key. `assignPending` is true right
* after "A" is pressed, waiting for the digit that completes the shortcut. */
onOpenAssign: () => void;
assignPending: boolean;
}
export function PlayView({
state,
album,
onToggle,
onNext,
onPrevious,
onSeek,
onVolume,
onMute,
onOpenAlbum,
onOpenAssign,
assignPending,
}: Props) {
const position = usePlaybackClock(state.position, state.playing);
const now = nowPlayingText(state, album);
const book = album ? isBook(album) : false;
const podcast = album ? groupOf(album) === "podcasts" : 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: SHOW_DECORATIVE_ANIMATIONS ? "dolphinSwim 62s linear infinite" : "none",
// Freezes in its current pose rather than snapping back to frame 0 -
// `animation: "none"` would restart it, `animationPlayState` just pauses.
animationPlayState: state.playing ? "running" : "paused",
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 ? (
<button
onClick={onOpenAlbum}
aria-label="Titelliste anzeigen"
style={{
border: "none",
background: "none",
padding: 0,
cursor: "pointer",
display: "flex",
height: "100%",
}}
>
<Cover
album={album}
size={340}
fit="height"
radius={book ? "18px 34px 34px 18px" : "28px"}
label
/>
</button>
) : (
<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",
}}
>
{now.title}
</div>
<div
style={{
fontSize: 20,
fontWeight: 700,
color: "oklch(88% 0.02 210 / .8)",
marginTop: 8,
}}
>
{now.subtitle}
</div>
{album && (
<div
style={{
fontSize: 15,
fontWeight: 800,
color: "var(--accent-dim)",
marginTop: 6,
}}
>
{now.progress}
</div>
)}
</div>
<div style={{ width: "min(680px, 90%)", position: "relative", zIndex: 1 }}>
<ProgressBar
position={position}
duration={state.duration}
onSeek={onSeek}
resetKey={now.resetKey}
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 && !podcast ? `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>
{album && (
<button
onClick={onOpenAssign}
aria-label={assignPending ? "Zahl drücken …" : "Taste zuweisen"}
title="Einer Fernbedienungs-Taste zuweisen (oder: A und dann eine Zahl)"
style={{
position: "absolute",
bottom: 28,
right: 32,
border: "none",
cursor: "pointer",
width: 52,
height: 52,
borderRadius: 999,
background: assignPending ? "var(--accent)" : "oklch(97% 0.01 210 / .95)",
color: assignPending ? "#fff" : "var(--ink)",
fontSize: 22,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 8px 24px oklch(15% 0.05 210 / .4)",
}}
>
🎛
</button>
)}
</div>
</div>
);
}