150 lines
3.9 KiB
TypeScript
150 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
useAudioPlayer,
|
|
type AudioTrack,
|
|
AudioWaveAnimation,
|
|
} from "@/components/audio-player";
|
|
import {
|
|
rememberedQueue,
|
|
useTranscriptQueue,
|
|
yearQueue,
|
|
} from "@/components/audio-player/transcript-queue";
|
|
import { cn } from "@/lib/utils";
|
|
import { Play } from "lucide-react";
|
|
|
|
type PlayAudioButtonProps = {
|
|
audioUrl: string;
|
|
title: string;
|
|
subtitle?: string;
|
|
sourceUrl: string;
|
|
type?: string;
|
|
className?: string;
|
|
/** Compact icon-only variant for list items */
|
|
variant?: "default" | "compact";
|
|
/**
|
|
* On a lecture's own page: its year, queued as a fallback when the lecture
|
|
* wasn't opened from a list (search, a shared link…).
|
|
*/
|
|
fallbackYear?: number | null;
|
|
};
|
|
|
|
export function PlayAudioButton({
|
|
audioUrl,
|
|
title,
|
|
subtitle,
|
|
sourceUrl,
|
|
type,
|
|
className,
|
|
variant = "default",
|
|
fallbackYear,
|
|
}: PlayAudioButtonProps) {
|
|
const {
|
|
currentTrack,
|
|
isPlaying,
|
|
play,
|
|
playPlaylist,
|
|
attachQueue,
|
|
togglePlayPause,
|
|
} = useAudioPlayer();
|
|
// The list this button sits in (a transcription list row), if any.
|
|
const listQueue = useTranscriptQueue();
|
|
|
|
const isThisTrack = currentTrack?.url === audioUrl;
|
|
const isThisPlaying = isThisTrack && isPlaying;
|
|
|
|
function handleClick() {
|
|
if (isThisTrack) {
|
|
togglePlayPause();
|
|
} else {
|
|
const track: AudioTrack = {
|
|
url: audioUrl,
|
|
title,
|
|
subtitle,
|
|
sourceUrl,
|
|
type: type ?? "lecture",
|
|
};
|
|
// Queue the list this recording was found in, so Next / Previous
|
|
// follow it: the list the button sits in, or — on a lecture's own
|
|
// page — the list it was opened from.
|
|
const queue =
|
|
listQueue ??
|
|
rememberedQueue(sourceUrl.replace(/^\/transcriptions\//, ""));
|
|
const at = queue?.findIndex((q) => q.url === audioUrl) ?? -1;
|
|
if (queue && at >= 0 && queue.length > 1) {
|
|
playPlaylist(queue, at);
|
|
return;
|
|
}
|
|
// No list to follow: start this lecture now — audio must begin inside
|
|
// the tap, or iOS blocks it — then queue its year once that loads.
|
|
play(track);
|
|
if (fallbackYear) {
|
|
void yearQueue(fallbackYear).then((tracks) => {
|
|
if (tracks && tracks.length > 1) attachQueue(tracks);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (variant === "compact") {
|
|
return (
|
|
<button
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleClick();
|
|
}}
|
|
className={cn(
|
|
"flex items-center justify-center size-8 rounded-full transition-all shrink-0 cursor-pointer!",
|
|
isThisPlaying
|
|
? "bg-primary text-primary-foreground shadow-sm"
|
|
: "bg-primary/10 text-primary hover:bg-primary/20",
|
|
className,
|
|
)}
|
|
title={isThisPlaying ? "Pause" : "Play audio"}
|
|
aria-label={isThisPlaying ? "Pause" : "Play audio"}
|
|
>
|
|
{isThisPlaying ? (
|
|
<AudioWaveAnimation
|
|
paused={false}
|
|
colorClassName="bg-primary-foreground"
|
|
size="sm"
|
|
/>
|
|
) : (
|
|
<Play className="size-3.5 ml-0.5" />
|
|
)}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<button
|
|
onClick={handleClick}
|
|
className={cn(
|
|
"inline-flex cursor-pointer items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-semibold transition-all shadow-sm",
|
|
isThisPlaying
|
|
? "bg-primary text-primary-foreground shadow-md"
|
|
: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md",
|
|
className,
|
|
)}
|
|
aria-label={isThisPlaying ? "Pause audio" : "Play audio"}
|
|
>
|
|
{isThisPlaying ? (
|
|
<>
|
|
<AudioWaveAnimation
|
|
paused={false}
|
|
colorClassName="bg-primary-foreground"
|
|
size="sm"
|
|
/>
|
|
<span>Now Playing</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play className="size-4" />
|
|
<span>Play Audio</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|