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) => {
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue