134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import { useEffect, useState, useRef, useCallback } from "react";
|
|
import { PitchContour } from "@renderer/components";
|
|
import WaveSurfer from "wavesurfer.js";
|
|
import { Button, Skeleton } from "@renderer/components/ui";
|
|
import { PlayIcon, PauseIcon } from "lucide-react";
|
|
import { useIntersectionObserver } from "@uidotdev/usehooks";
|
|
import { secondsToTimestamp } from "@renderer/lib/utils";
|
|
|
|
export const PostRecording = (props: {
|
|
recording: RecordingType;
|
|
height?: number;
|
|
}) => {
|
|
const { recording, height = 80 } = props;
|
|
const [initialized, setInitialized] = useState(false);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [wavesurfer, setWavesurfer] = useState(null);
|
|
const containerRef = useRef();
|
|
const [ref, entry] = useIntersectionObserver({
|
|
threshold: 1,
|
|
});
|
|
const [duration, setDuration] = useState<number>(0);
|
|
|
|
const onPlayClick = useCallback(() => {
|
|
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play();
|
|
}, [wavesurfer]);
|
|
|
|
useEffect(() => {
|
|
// use the intersection observer to only create the wavesurfer instance
|
|
// when the player is visible
|
|
if (!entry?.isIntersecting) return;
|
|
if (!recording.src) return;
|
|
if (wavesurfer) return;
|
|
|
|
const ws = WaveSurfer.create({
|
|
container: containerRef.current,
|
|
url: recording.src,
|
|
height,
|
|
barWidth: 1,
|
|
cursorWidth: 0,
|
|
autoCenter: true,
|
|
autoScroll: true,
|
|
dragToSeek: true,
|
|
hideScrollbar: true,
|
|
minPxPerSec: 100,
|
|
waveColor: "rgba(0, 0, 0, 0.25)",
|
|
progressColor: "rgba(0, 0, 0, 0.5)",
|
|
});
|
|
|
|
setWavesurfer(ws);
|
|
}, [recording.src, entry]);
|
|
|
|
useEffect(() => {
|
|
if (!wavesurfer) return;
|
|
|
|
const subscriptions = [
|
|
wavesurfer.on("play", () => {
|
|
setIsPlaying(true);
|
|
}),
|
|
wavesurfer.on("pause", () => {
|
|
setIsPlaying(false);
|
|
}),
|
|
wavesurfer.on("decode", () => {
|
|
setDuration(wavesurfer.getDuration());
|
|
const peaks = wavesurfer.getDecodedData().getChannelData(0);
|
|
const sampleRate = wavesurfer.options.sampleRate;
|
|
wavesurfer.renderer.getWrapper().appendChild(
|
|
PitchContour({
|
|
peaks,
|
|
sampleRate,
|
|
height,
|
|
})
|
|
);
|
|
setInitialized(true);
|
|
}),
|
|
];
|
|
|
|
return () => {
|
|
subscriptions.forEach((unsub) => unsub());
|
|
wavesurfer?.destroy();
|
|
};
|
|
}, [wavesurfer]);
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<div className="flex justify-end">
|
|
<span className="text-xs text-muted-foreground">
|
|
{secondsToTimestamp(duration)}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
ref={ref}
|
|
className="bg-sky-500/30 rounded-lg grid grid-cols-9 items-center relative h-[80px]"
|
|
>
|
|
{!initialized && (
|
|
<div className="col-span-9 flex flex-col justify-around h-[80px]">
|
|
<Skeleton className="h-2 w-full rounded-full" />
|
|
<Skeleton className="h-2 w-full rounded-full" />
|
|
<Skeleton className="h-2 w-full rounded-full" />
|
|
</div>
|
|
)}
|
|
|
|
<div className={`flex justify-center ${initialized ? "" : "hidden"}`}>
|
|
<Button
|
|
onClick={onPlayClick}
|
|
className="aspect-square rounded-full p-2 w-12 h-12 bg-blue-600 hover:bg-blue-500"
|
|
>
|
|
{isPlaying ? (
|
|
<PauseIcon className="w-6 h-6 text-white" />
|
|
) : (
|
|
<PlayIcon className="w-6 h-6 text-white" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
<div
|
|
className={`col-span-8 ${initialized ? "" : "hidden"}`}
|
|
ref={containerRef}
|
|
></div>
|
|
</div>
|
|
|
|
{
|
|
recording.referenceText && (
|
|
<div className="mt-2 bg-muted px-4 py-2 rounded">
|
|
<div className="text-muted-foreground text-center font-serif">
|
|
{recording.referenceText}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
);
|
|
};
|