feat: delete a single audio entry (#119)
* feat: delete a single audio entry * fix(HistorySetting): remove unnecessary loading state
This commit is contained in:
parent
3ae70a4288
commit
9185b3db08
4 changed files with 97 additions and 3 deletions
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue