feat: delete a single audio entry (#119)

* feat: delete a single audio entry

* fix(HistorySetting): remove unnecessary loading state
This commit is contained in:
Han Pham 2025-09-19 12:30:50 -07:00 committed by GitHub
parent 3ae70a4288
commit 9185b3db08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 97 additions and 3 deletions

View file

@ -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<HistoryManager>>,
id: i64,
) -> Result<(), String> {
history_manager
.as_ref()
.delete_entry(id)
.await
.map_err(|e| e.to_string())
}

View file

@ -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");

View file

@ -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<Option<HistoryEntry>> {
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

View file

@ -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 (
<div className="max-w-3xl w-full mx-auto space-y-6">
@ -118,6 +127,7 @@ export const HistorySettings: React.FC = () => {
onToggleSaved={() => toggleSaved(entry.id)}
onCopyText={() => copyToClipboard(entry.transcription_text)}
getAudioUrl={getAudioUrl}
deleteAudio={deleteAudioEntry}
/>
))}
</SettingsGroup>
@ -130,6 +140,7 @@ interface HistoryEntryProps {
onToggleSaved: () => void;
onCopyText: () => void;
getAudioUrl: (fileName: string) => Promise<string | null>;
deleteAudio: (id: number) => Promise<void>;
}
const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
@ -137,6 +148,7 @@ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
onToggleSaved,
onCopyText,
getAudioUrl,
deleteAudio,
}) => {
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const [showCopied, setShowCopied] = useState(false);
@ -155,6 +167,15 @@ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
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">
@ -186,6 +207,13 @@ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
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">