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

View file

@ -2,20 +2,29 @@ import React, { useState, useRef, useEffect, useCallback } from "react";
import { Play, Pause } from "lucide-react";
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;
autoPlay?: boolean;
}
export const AudioPlayer: React.FC<AudioPlayerProps> = ({
src,
src: initialSrc,
onLoadRequest,
className = "",
autoPlay = false,
}) => {
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [loadedSrc, setLoadedSrc] = useState<string | null>(initialSrc ?? null);
const [isLoading, setIsLoading] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null);
const src = loadedSrc;
const animationRef = useRef<number>();
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
const handleMouseUp = useCallback(() => {
if (isDragging) {
@ -121,15 +152,36 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
}
}, [isDragging, handleMouseUp]);
// Cleanup blob URLs on unmount
useEffect(() => {
return () => {
if (loadedSrc?.startsWith("blob:")) {
URL.revokeObjectURL(loadedSrc);
}
};
}, [loadedSrc]);
const togglePlay = async () => {
const audio = audioRef.current;
if (!audio) return;
if (isLoading) return;
try {
if (isPlaying) {
audio.pause();
} 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) {
console.error("Playback failed:", error);
@ -177,11 +229,12 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
return (
<div className={`flex items-center gap-3 ${className}`}>
<audio ref={audioRef} src={src} preload="metadata" />
<audio ref={audioRef} src={src ?? undefined} preload="metadata" />
<button
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"}
>
{isPlaying ? (