From 5ba5f77b45a1566a192bd9161957f1d3f486b642 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sun, 1 Feb 2026 13:03:13 +0800 Subject: [PATCH] basic working fix of history (#691) * basic working fix of history * format * fix weird jank + cleanup impl --- .../settings/history/HistorySettings.tsx | 67 +++++++------------ src/components/ui/AudioPlayer.tsx | 63 +++++++++++++++-- 2 files changed, 82 insertions(+), 48 deletions(-) diff --git a/src/components/settings/history/HistorySettings.tsx b/src/components/settings/history/HistorySettings.tsx index cf75c17..b8f7fd0 100644 --- a/src/components/settings/history/HistorySettings.tsx +++ b/src/components/settings/history/HistorySettings.tsx @@ -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 = ({ deleteAudio, }) => { const { t, i18n } = useTranslation(); - const [audioUrl, setAudioUrl] = useState(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 = ({

{entry.transcription_text}

- {audioUrl && } + ); }; diff --git a/src/components/ui/AudioPlayer.tsx b/src/components/ui/AudioPlayer.tsx index 0b2e9f8..884452a 100644 --- a/src/components/ui/AudioPlayer.tsx +++ b/src/components/ui/AudioPlayer.tsx @@ -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; className?: string; + autoPlay?: boolean; } export const AudioPlayer: React.FC = ({ - 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(initialSrc ?? null); + const [isLoading, setIsLoading] = useState(false); const audioRef = useRef(null); + const src = loadedSrc; const animationRef = useRef(); const dragTimeRef = useRef(0); @@ -98,6 +107,28 @@ export const AudioPlayer: React.FC = ({ }; }, []); + // Auto-play when src becomes available (via onLoadRequest or autoPlay prop) + const prevLoadedSrc = useRef(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 = ({ } }, [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 = ({ return (
-