diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index 6c4ee2d..befa3f7 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -36,3 +36,16 @@ pub async fn get_audio_file_path( .ok_or_else(|| "Invalid file path".to_string()) .map(|s| s.to_string()) } + +#[tauri::command] +pub async fn delete_history_entry( + _app: AppHandle, + history_manager: State<'_, Arc>, + id: i64, +) -> Result<(), String> { + history_manager + .as_ref() + .delete_entry(id) + .await + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e61d10d..324ff8c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -241,7 +241,8 @@ pub fn run() { commands::transcription::unload_model_manually, commands::history::get_history_entries, commands::history::toggle_history_entry_saved, - commands::history::get_audio_file_path + commands::history::get_audio_file_path, + commands::history::delete_history_entry ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/managers/history.rs b/src-tauri/src/managers/history.rs index b524dc3..cd869e8 100644 --- a/src-tauri/src/managers/history.rs +++ b/src-tauri/src/managers/history.rs @@ -1,7 +1,7 @@ use anyhow::Result; use chrono::{DateTime, Local, Utc}; use log::{debug, error}; -use rusqlite::{params, Connection}; +use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -238,6 +238,58 @@ impl HistoryManager { self.recordings_dir.join(file_name) } + pub async fn get_entry_by_id(&self, id: i64) -> Result> { + let conn = self.get_connection()?; + let mut stmt = conn.prepare( + "SELECT id, file_name, timestamp, saved, title, transcription_text + FROM transcription_history WHERE id = ?1" + )?; + + let entry = stmt.query_row([id], |row| { + Ok(HistoryEntry { + id: row.get("id")?, + file_name: row.get("file_name")?, + timestamp: row.get("timestamp")?, + saved: row.get("saved")?, + title: row.get("title")?, + transcription_text: row.get("transcription_text")?, + }) + }).optional()?; + + Ok(entry) + } + + pub async fn delete_entry(&self, id: i64) -> Result<()> { + let conn = self.get_connection()?; + + // Get the entry to find the file name + if let Some(entry) = self.get_entry_by_id(id).await? { + // Delete the audio file first + let file_path = self.get_audio_file_path(&entry.file_name); + if file_path.exists() { + if let Err(e) = fs::remove_file(&file_path) { + error!("Failed to delete audio file {}: {}", entry.file_name, e); + // Continue with database deletion even if file deletion fails + } + } + } + + // Delete from database + conn.execute( + "DELETE FROM transcription_history WHERE id = ?1", + params![id], + )?; + + debug!("Deleted history entry with id: {}", id); + + // Emit history updated event + if let Err(e) = self.app_handle.emit("history-updated", ()) { + error!("Failed to emit history-updated event: {}", e); + } + + Ok(()) + } + fn format_timestamp_title(&self, timestamp: i64) -> String { if let Some(utc_datetime) = DateTime::from_timestamp(timestamp, 0) { // Convert UTC to local timezone diff --git a/src/components/settings/HistorySettings.tsx b/src/components/settings/HistorySettings.tsx index 8c9e018..b78aec3 100644 --- a/src/components/settings/HistorySettings.tsx +++ b/src/components/settings/HistorySettings.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { SettingsGroup } from "../ui/SettingsGroup"; import { AudioPlayer } from "../ui/AudioPlayer"; -import { ClipboardCopy, Star, Check } from "lucide-react"; +import { ClipboardCopy, Star, Check, Trash2, Loader2 } from "lucide-react"; import { convertFileSrc, invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; @@ -84,6 +84,15 @@ export const HistorySettings: React.FC = () => { } }; + const deleteAudioEntry = async (id: number) => { + try { + await invoke("delete_history_entry", {id}); + } catch (error) { + console.error("Failed to delete audio entry:", error); + throw error; + } + } + if (loading) { return (
@@ -118,6 +127,7 @@ export const HistorySettings: React.FC = () => { onToggleSaved={() => toggleSaved(entry.id)} onCopyText={() => copyToClipboard(entry.transcription_text)} getAudioUrl={getAudioUrl} + deleteAudio={deleteAudioEntry} /> ))} @@ -130,6 +140,7 @@ interface HistoryEntryProps { onToggleSaved: () => void; onCopyText: () => void; getAudioUrl: (fileName: string) => Promise; + deleteAudio: (id: number) => Promise; } const HistoryEntryComponent: React.FC = ({ @@ -137,6 +148,7 @@ const HistoryEntryComponent: React.FC = ({ onToggleSaved, onCopyText, getAudioUrl, + deleteAudio, }) => { const [audioUrl, setAudioUrl] = useState(null); const [showCopied, setShowCopied] = useState(false); @@ -155,6 +167,15 @@ const HistoryEntryComponent: React.FC = ({ 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 (
@@ -186,6 +207,13 @@ const HistoryEntryComponent: React.FC = ({ fill={entry.saved ? "currentColor" : "none"} /> +