* initial specta commands * refactor to use the bindings in the ui * merge fixes * remove unused commands * add clamshell to lib * Remove Invoke from everything. * fix settings not being loaded properly * fix settings being overwritten due to deserialization failure + remove provider kind * fix model loading text bug
278 lines
8.2 KiB
TypeScript
278 lines
8.2 KiB
TypeScript
import React, { useState, useEffect, useCallback } from "react";
|
|
import { AudioPlayer } from "../../ui/AudioPlayer";
|
|
import { Button } from "../../ui/Button";
|
|
import { Copy, Star, Check, Trash2, FolderOpen } from "lucide-react";
|
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import { commands, type HistoryEntry } from "@/bindings";
|
|
|
|
interface OpenRecordingsButtonProps {
|
|
onClick: () => void;
|
|
}
|
|
|
|
const OpenRecordingsButton: React.FC<OpenRecordingsButtonProps> = ({
|
|
onClick,
|
|
}) => (
|
|
<Button
|
|
onClick={onClick}
|
|
variant="secondary"
|
|
size="sm"
|
|
className="flex items-center gap-2"
|
|
title="Open recordings folder"
|
|
>
|
|
<FolderOpen className="w-4 h-4" />
|
|
<span>Open Recordings Folder</span>
|
|
</Button>
|
|
);
|
|
|
|
export const HistorySettings: React.FC = () => {
|
|
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const loadHistoryEntries = useCallback(async () => {
|
|
try {
|
|
const result = await commands.getHistoryEntries();
|
|
if (result.status === "ok") {
|
|
setHistoryEntries(result.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load history entries:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadHistoryEntries();
|
|
|
|
// Listen for history update events
|
|
const setupListener = async () => {
|
|
const unlisten = await listen("history-updated", () => {
|
|
console.log("History updated, reloading entries...");
|
|
loadHistoryEntries();
|
|
});
|
|
|
|
// Return cleanup function
|
|
return unlisten;
|
|
};
|
|
|
|
let unlistenPromise = setupListener();
|
|
|
|
return () => {
|
|
unlistenPromise.then((unlisten) => {
|
|
if (unlisten) {
|
|
unlisten();
|
|
}
|
|
});
|
|
};
|
|
}, [loadHistoryEntries]);
|
|
|
|
const toggleSaved = async (id: string) => {
|
|
try {
|
|
await commands.toggleHistoryEntrySaved(id);
|
|
// No need to reload here - the event listener will handle it
|
|
} catch (error) {
|
|
console.error("Failed to toggle saved status:", error);
|
|
}
|
|
};
|
|
|
|
const copyToClipboard = async (text: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
} catch (error) {
|
|
console.error("Failed to copy to clipboard:", error);
|
|
}
|
|
};
|
|
|
|
const getAudioUrl = async (fileName: string) => {
|
|
try {
|
|
const result = await commands.getAudioFilePath(fileName);
|
|
if (result.status === "ok") {
|
|
return convertFileSrc(`${result.data}`, "asset");
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error("Failed to get audio file path:", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const deleteAudioEntry = async (id: string) => {
|
|
try {
|
|
await commands.deleteHistoryEntry(id);
|
|
} catch (error) {
|
|
console.error("Failed to delete audio entry:", error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const openRecordingsFolder = async () => {
|
|
try {
|
|
await commands.openRecordingsFolder();
|
|
} catch (error) {
|
|
console.error("Failed to open recordings folder:", error);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="max-w-3xl w-full mx-auto space-y-6">
|
|
<div className="space-y-2">
|
|
<div className="px-4 flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xs font-medium text-mid-gray uppercase tracking-wide">
|
|
History
|
|
</h2>
|
|
</div>
|
|
<OpenRecordingsButton onClick={openRecordingsFolder} />
|
|
</div>
|
|
<div className="bg-background border border-mid-gray/20 rounded-lg overflow-visible">
|
|
<div className="px-4 py-3 text-center text-text/60">
|
|
Loading history...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (historyEntries.length === 0) {
|
|
return (
|
|
<div className="max-w-3xl w-full mx-auto space-y-6">
|
|
<div className="space-y-2">
|
|
<div className="px-4 flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xs font-medium text-mid-gray uppercase tracking-wide">
|
|
History
|
|
</h2>
|
|
</div>
|
|
<OpenRecordingsButton onClick={openRecordingsFolder} />
|
|
</div>
|
|
<div className="bg-background border border-mid-gray/20 rounded-lg overflow-visible">
|
|
<div className="px-4 py-3 text-center text-text/60">
|
|
No transcriptions yet. Start recording to build your history!
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-3xl w-full mx-auto space-y-6">
|
|
<div className="space-y-2">
|
|
<div className="px-4 flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xs font-medium text-mid-gray uppercase tracking-wide">
|
|
History
|
|
</h2>
|
|
</div>
|
|
<OpenRecordingsButton onClick={openRecordingsFolder} />
|
|
</div>
|
|
<div className="bg-background border border-mid-gray/20 rounded-lg overflow-visible">
|
|
<div className="divide-y divide-mid-gray/20">
|
|
{historyEntries.map((entry) => (
|
|
<HistoryEntryComponent
|
|
key={entry.id}
|
|
entry={entry}
|
|
onToggleSaved={() => toggleSaved(entry.id)}
|
|
onCopyText={() => copyToClipboard(entry.transcription_text)}
|
|
getAudioUrl={getAudioUrl}
|
|
deleteAudio={deleteAudioEntry}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface HistoryEntryProps {
|
|
entry: HistoryEntry;
|
|
onToggleSaved: () => void;
|
|
onCopyText: () => void;
|
|
getAudioUrl: (fileName: string) => Promise<string | null>;
|
|
deleteAudio: (id: string) => Promise<void>;
|
|
}
|
|
|
|
const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
|
|
entry,
|
|
onToggleSaved,
|
|
onCopyText,
|
|
getAudioUrl,
|
|
deleteAudio,
|
|
}) => {
|
|
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
|
const [showCopied, setShowCopied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const loadAudio = async () => {
|
|
const url = await getAudioUrl(entry.file_name);
|
|
setAudioUrl(url);
|
|
};
|
|
loadAudio();
|
|
}, [entry.file_name, getAudioUrl]);
|
|
|
|
const handleCopyText = () => {
|
|
onCopyText();
|
|
setShowCopied(true);
|
|
setTimeout(() => setShowCopied(false), 2000);
|
|
};
|
|
|
|
const handleDeleteEntry = async () => {
|
|
try {
|
|
await deleteAudio(entry.id);
|
|
} catch (error) {
|
|
console.error("Failed to delete entry:", error);
|
|
alert("Failed to delete entry. Please try again.");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="px-4 py-2 pb-5 flex flex-col gap-3">
|
|
<div className="flex justify-between items-center">
|
|
<p className="text-sm font-medium">{entry.title}</p>
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={handleCopyText}
|
|
className="text-text/50 hover:text-logo-primary hover:border-logo-primary transition-colors cursor-pointer"
|
|
title="Copy transcription to clipboard"
|
|
>
|
|
{showCopied ? (
|
|
<Check width={16} height={16} />
|
|
) : (
|
|
<Copy width={16} height={16} />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={onToggleSaved}
|
|
className={`p-2 rounded transition-colors cursor-pointer ${
|
|
entry.saved
|
|
? "text-logo-primary hover:text-logo-primary/80"
|
|
: "text-text/50 hover:text-logo-primary"
|
|
}`}
|
|
title={entry.saved ? "Remove from saved" : "Save transcription"}
|
|
>
|
|
<Star
|
|
width={16}
|
|
height={16}
|
|
fill={entry.saved ? "currentColor" : "none"}
|
|
/>
|
|
</button>
|
|
<button
|
|
onClick={handleDeleteEntry}
|
|
className="text-text/50 hover:text-logo-primary transition-colors cursor-pointer"
|
|
title="Delete entry"
|
|
>
|
|
<Trash2 width={16} height={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p className="italic text-text/90 text-sm pb-2">
|
|
{entry.transcription_text}
|
|
</p>
|
|
{audioUrl && <AudioPlayer src={audioUrl} className="w-full" />}
|
|
</div>
|
|
);
|
|
};
|