basic working fix of history (#691)

* basic working fix of history

* format

* fix weird jank + cleanup impl
This commit is contained in:
CJ Pais 2026-02-01 13:03:13 +08:00 committed by GitHub
parent a033a67b13
commit 5ba5f77b45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 48 deletions

View file

@ -92,25 +92,28 @@ export const HistorySettings: React.FC = () => {
} }
}; };
const getAudioUrl = async (fileName: string) => { const getAudioUrl = useCallback(
try { async (fileName: string) => {
const result = await commands.getAudioFilePath(fileName); try {
if (result.status === "ok") { const result = await commands.getAudioFilePath(fileName);
if (osType === "linux") { if (result.status === "ok") {
const fileData = await readFile(result.data); if (osType === "linux") {
const blob = new Blob([fileData], { type: "audio/wav" }); const fileData = await readFile(result.data);
const blob = new Blob([fileData], { type: "audio/wav" });
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
}
return convertFileSrc(result.data, "asset");
} }
return null;
return convertFileSrc(result.data, "asset"); } catch (error) {
console.error("Failed to get audio file path:", error);
return null;
} }
return null; },
} catch (error) { [osType],
console.error("Failed to get audio file path:", error); );
return null;
}
};
const deleteAudioEntry = async (id: number) => { const deleteAudioEntry = async (id: number) => {
try { try {
@ -228,34 +231,12 @@ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
deleteAudio, deleteAudio,
}) => { }) => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const [showCopied, setShowCopied] = useState(false); const [showCopied, setShowCopied] = useState(false);
useEffect(() => { const handleLoadAudio = useCallback(
let cancelled = false; () => getAudioUrl(entry.file_name),
let urlToRevoke: string | null = null; [getAudioUrl, entry.file_name],
);
const loadAudio = async () => {
const url = await getAudioUrl(entry.file_name);
if (!cancelled) {
urlToRevoke = url;
setAudioUrl(url);
} else if (url?.startsWith("blob:")) {
URL.revokeObjectURL(url);
}
};
loadAudio();
return () => {
cancelled = true;
if (urlToRevoke?.startsWith("blob:")) {
URL.revokeObjectURL(urlToRevoke);
}
};
}, [entry.file_name]);
const handleCopyText = () => { const handleCopyText = () => {
onCopyText(); onCopyText();
@ -321,7 +302,7 @@ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
<p className="italic text-text/90 text-sm pb-2 select-text cursor-text"> <p className="italic text-text/90 text-sm pb-2 select-text cursor-text">
{entry.transcription_text} {entry.transcription_text}
</p> </p>
{audioUrl && <AudioPlayer src={audioUrl} className="w-full" />} <AudioPlayer onLoadRequest={handleLoadAudio} className="w-full" />
</div> </div>
); );
}; };

View file

@ -2,20 +2,29 @@ import React, { useState, useRef, useEffect, useCallback } from "react";
import { Play, Pause } from "lucide-react"; import { Play, Pause } from "lucide-react";
interface AudioPlayerProps { interface AudioPlayerProps {
src: string; /** Audio source URL. If not provided, onLoadRequest must be provided. */
src?: string;
/** Called when play is clicked and no src is loaded yet. Should return the audio URL. */
onLoadRequest?: () => Promise<string | null>;
className?: string; className?: string;
autoPlay?: boolean;
} }
export const AudioPlayer: React.FC<AudioPlayerProps> = ({ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
src, src: initialSrc,
onLoadRequest,
className = "", className = "",
autoPlay = false,
}) => { }) => {
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [loadedSrc, setLoadedSrc] = useState<string | null>(initialSrc ?? null);
const [isLoading, setIsLoading] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
const src = loadedSrc;
const animationRef = useRef<number>(); const animationRef = useRef<number>();
const dragTimeRef = useRef<number>(0); const dragTimeRef = useRef<number>(0);
@ -98,6 +107,28 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
}; };
}, []); }, []);
// Auto-play when src becomes available (via onLoadRequest or autoPlay prop)
const prevLoadedSrc = useRef<string | null>(null);
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
// Play when loadedSrc changes from null to a value (lazy load case)
if (loadedSrc && !prevLoadedSrc.current && onLoadRequest) {
audio.play().catch((error) => {
console.error("Auto-play failed:", error);
});
}
// Or when autoPlay is set with initial src
else if (autoPlay && initialSrc && !prevLoadedSrc.current) {
audio.play().catch((error) => {
console.error("Auto-play failed:", error);
});
}
prevLoadedSrc.current = loadedSrc;
}, [loadedSrc, autoPlay, initialSrc, onLoadRequest]);
// Global drag handlers // Global drag handlers
const handleMouseUp = useCallback(() => { const handleMouseUp = useCallback(() => {
if (isDragging) { if (isDragging) {
@ -121,15 +152,36 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
} }
}, [isDragging, handleMouseUp]); }, [isDragging, handleMouseUp]);
// Cleanup blob URLs on unmount
useEffect(() => {
return () => {
if (loadedSrc?.startsWith("blob:")) {
URL.revokeObjectURL(loadedSrc);
}
};
}, [loadedSrc]);
const togglePlay = async () => { const togglePlay = async () => {
const audio = audioRef.current; const audio = audioRef.current;
if (!audio) return; if (!audio) return;
if (isLoading) return;
try { try {
if (isPlaying) { if (isPlaying) {
audio.pause(); audio.pause();
} else { } else {
await audio.play(); // If no src loaded yet, request it
if (!src && onLoadRequest) {
setIsLoading(true);
const newSrc = await onLoadRequest();
setIsLoading(false);
if (newSrc) {
setLoadedSrc(newSrc);
// Playback will be triggered by the useEffect watching loadedSrc
}
} else if (src) {
await audio.play();
}
} }
} catch (error) { } catch (error) {
console.error("Playback failed:", error); console.error("Playback failed:", error);
@ -177,11 +229,12 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
return ( return (
<div className={`flex items-center gap-3 ${className}`}> <div className={`flex items-center gap-3 ${className}`}>
<audio ref={audioRef} src={src} preload="metadata" /> <audio ref={audioRef} src={src ?? undefined} preload="metadata" />
<button <button
onClick={togglePlay} onClick={togglePlay}
className="transition-colors cursor-pointer text-text hover:text-logo-primary" disabled={isLoading}
className="transition-colors cursor-pointer text-text hover:text-logo-primary disabled:opacity-50"
aria-label={isPlaying ? "Pause" : "Play"} aria-label={isPlaying ? "Pause" : "Play"}
> >
{isPlaying ? ( {isPlaying ? (