Files
everyone-can-use-english/enjoy/src/renderer/components/posts/post-audio.tsx
2024-01-13 16:32:12 +08:00

161 lines
4.7 KiB
TypeScript

import { useEffect, useState, useRef, useCallback, useContext } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
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 PostAudio = (props: {
audio: Partial<MediumType>;
height?: number;
}) => {
const { audio, 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 { webApi } = useContext(AppSettingsProviderContext);
const [currentTime, setCurrentTime] = useState<number>(0);
const [transcription, setTranscription] = useState<TranscriptionType>();
const currentTranscription = (transcription?.result || []).find(
(s) =>
currentTime >= s.offsets.from / 1000.0 &&
currentTime <= s.offsets.to / 1000.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 (!audio.sourceUrl) return;
if (wavesurfer) return;
const ws = WaveSurfer.create({
container: containerRef.current,
url: audio.sourceUrl,
height,
barWidth: 1,
cursorWidth: 0,
autoCenter: true,
autoScroll: true,
dragToSeek: true,
hideScrollbar: true,
minPxPerSec: 100,
waveColor: "#ddd",
progressColor: "rgba(0, 0, 0, 0.25)",
});
setWavesurfer(ws);
}, [audio.sourceUrl, entry]);
useEffect(() => {
if (!wavesurfer) return;
const subscriptions = [
wavesurfer.on("play", () => {
setIsPlaying(true);
}),
wavesurfer.on("pause", () => {
setIsPlaying(false);
}),
wavesurfer.on("timeupdate", (time: number) => {
setCurrentTime(time);
}),
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]);
useEffect(() => {
webApi
.transcriptions({
targetMd5: audio.md5,
})
.then((response) => {
setTranscription(response?.transcriptions?.[0]);
});
}, [audio.md5]);
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-white 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-3 w-full rounded-full" />
<Skeleton className="h-3 w-full rounded-full" />
<Skeleton className="h-3 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>
{currentTranscription && (
<div className="mt-2 bg-muted px-4 py-2 rounded">
<div className="text-muted-foreground text-center font-serif">
{currentTranscription.text}
</div>
</div>
)}
{audio.coverUrl && (
<div className="">
<img src={audio.coverUrl} className="w-full rounded" />
</div>
)}
</div>
);
};