basic working fix of history (#691)
* basic working fix of history * format * fix weird jank + cleanup impl
This commit is contained in:
parent
a033a67b13
commit
5ba5f77b45
2 changed files with 82 additions and 48 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
Loading…
Reference in a new issue