v1 review pass: fixed some minor bugs

This commit is contained in:
khannurien
2026-03-16 11:08:39 +00:00
parent e88fed4e98
commit 867e64cb5b
37 changed files with 1228 additions and 400 deletions

View File

@@ -23,8 +23,12 @@ const IconPause = () => (
const IconVolume = ({ muted }: { muted: boolean }) => (
<svg viewBox="0 0 24 24" fill="currentColor">
{muted
? <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM19 12c0 2.76-1.67 5.12-4 6.19V5.81C17.33 6.88 19 9.24 19 12z M19 12l2.5 2.5M21.5 12 19 14.5" />
: <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM19 12c0 2.76-1.67 5.12-4 6.19V5.81C17.33 6.88 19 9.24 19 12z" />}
? (
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM19 12c0 2.76-1.67 5.12-4 6.19V5.81C17.33 6.88 19 9.24 19 12z M19 12l2.5 2.5M21.5 12 19 14.5" />
)
: (
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM19 12c0 2.76-1.67 5.12-4 6.19V5.81C17.33 6.88 19 9.24 19 12z" />
)}
</svg>
);
@@ -55,7 +59,9 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
useEffect(() => {
const a = mediaRef.current!;
const onTime = () => { if (!dragging) setCurrent(a.currentTime); };
const onTime = () => {
if (!dragging) setCurrent(a.currentTime);
};
const onDuration = () => setDuration(a.duration);
const onEnded = () => setPlaying(false);
a.addEventListener("timeupdate", onTime);
@@ -68,31 +74,52 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
};
}, [dragging]);
// Show controls when paused; schedule hide when playing
// Stop any in-flight load on unmount.
useEffect(() => {
const a = mediaRef.current!;
return () => {
a.pause();
a.removeAttribute("src");
a.load();
};
}, []);
// Schedule controls hide when playing; controls are always visible when paused (derived below)
useEffect(() => {
if (kind !== "video") return;
if (hideTimer.current) clearTimeout(hideTimer.current);
if (playing) {
hideTimer.current = setTimeout(() => setControlsVisible(false), HIDE_DELAY);
} else {
setControlsVisible(true);
hideTimer.current = setTimeout(
() => setControlsVisible(false),
HIDE_DELAY,
);
}
return () => { if (hideTimer.current) clearTimeout(hideTimer.current); };
return () => {
if (hideTimer.current) clearTimeout(hideTimer.current);
};
}, [playing, kind]);
// Controls are always visible when paused or for audio; otherwise follow controlsVisible state
const showingControls = kind !== "video" || !playing || controlsVisible;
const showControlsTemporarily = () => {
if (kind !== "video") return;
setControlsVisible(true);
if (hideTimer.current) clearTimeout(hideTimer.current);
if (playing) {
hideTimer.current = setTimeout(() => setControlsVisible(false), HIDE_DELAY);
hideTimer.current = setTimeout(
() => setControlsVisible(false),
HIDE_DELAY,
);
}
};
const toggle = () => {
const a = mediaRef.current!;
if (playing) { a.pause(); setPlaying(false); }
else { a.play(); setPlaying(true); }
if (playing) {
a.pause();
setPlaying(false);
} else a.play().then(() => setPlaying(true)).catch(() => {});
};
const seek = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -105,7 +132,10 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
const v = Number(e.target.value);
setVolume(v);
mediaRef.current!.volume = v;
if (v > 0 && muted) { setMuted(false); mediaRef.current!.muted = false; }
if (v > 0 && muted) {
setMuted(false);
mediaRef.current!.muted = false;
}
};
const toggleMute = () => {
@@ -122,18 +152,29 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
const controls = (
<>
<button type="button" className="audio-player-btn" onClick={toggle} aria-label={playing ? "Pause" : "Play"}>
<button
type="button"
className="audio-player-btn"
onClick={toggle}
aria-label={playing ? "Pause" : "Play"}
>
{playing ? <IconPause /> : <IconPlay />}
</button>
<span className="audio-player-time">{fmt(current)}</span>
<div className="audio-player-track">
<div className="audio-player-fill" style={{ width: `${progress * 100}%` }} />
<div
className="audio-player-fill"
style={{ width: `${progress * 100}%` }}
/>
<input
type="range"
className="audio-player-range"
min={0} max={duration || 1} step={0.01} value={current}
min={0}
max={duration || 1}
step={0.01}
value={current}
onMouseDown={() => setDragging(true)}
onMouseUp={() => setDragging(false)}
onChange={seek}
@@ -144,15 +185,26 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
<span className="audio-player-time">{fmt(duration)}</span>
<div className="audio-player-volume">
<button type="button" className="audio-player-vol-btn" onClick={toggleMute} aria-label={muted ? "Unmute" : "Mute"}>
<button
type="button"
className="audio-player-vol-btn"
onClick={toggleMute}
aria-label={muted ? "Unmute" : "Mute"}
>
<IconVolume muted={muted} />
</button>
<div className="audio-player-track audio-player-track--volume">
<div className="audio-player-fill" style={{ width: `${(muted ? 0 : volume) * 100}%` }} />
<div
className="audio-player-fill"
style={{ width: `${(muted ? 0 : volume) * 100}%` }}
/>
<input
type="range"
className="audio-player-range"
min={0} max={1} step={0.01} value={muted ? 0 : volume}
min={0}
max={1}
step={0.01}
value={muted ? 0 : volume}
onChange={changeVolume}
aria-label="Volume"
/>
@@ -160,7 +212,12 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
</div>
{kind === "video" && (
<button type="button" className="audio-player-vol-btn" onClick={goFullscreen} aria-label="Fullscreen">
<button
type="button"
className="audio-player-vol-btn"
onClick={goFullscreen}
aria-label="Fullscreen"
>
<IconFullscreen />
</button>
)}
@@ -170,11 +227,12 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
if (kind === "video") {
return (
<div
className={`video-player${controlsVisible ? " video-player--controls-visible" : ""}`}
className={`video-player${
showingControls ? " video-player--controls-visible" : ""
}`}
onMouseMove={showControlsTemporarily}
onMouseLeave={() => playing && setControlsVisible(false)}
>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
ref={mediaRef as React.RefObject<HTMLVideoElement>}
src={src}
@@ -193,7 +251,13 @@ export function MediaPlayer({ src, kind, mime }: MediaPlayerProps) {
return (
<div className="audio-player">
<audio ref={mediaRef as React.RefObject<HTMLAudioElement>} src={src} preload="metadata" />
<audio
ref={mediaRef as React.RefObject<HTMLAudioElement>}
src={src}
preload="metadata"
>
{mime && <source src={src} type={mime} />}
</audio>
{controls}
</div>
);