Add history limit setting (#150)
* Add history limit setting Dearest Reviewer, I have added a history limit setting. The limit change is applied instantly. If the limit is 0 then no recordings are saved. Becker * remove extra save save happens on exit * move to debug menu * use the text color --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
1432bac30f
commit
d0e6a17c17
9 changed files with 136 additions and 40 deletions
|
|
@ -44,8 +44,24 @@ pub async fn delete_history_entry(
|
|||
id: i64,
|
||||
) -> Result<(), String> {
|
||||
history_manager
|
||||
.as_ref()
|
||||
.delete_entry(id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_history_limit(
|
||||
app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
limit: usize,
|
||||
) -> Result<(), String> {
|
||||
let mut settings = crate::settings::get_settings(&app);
|
||||
settings.history_limit = limit;
|
||||
crate::settings::write_settings(&app, settings);
|
||||
|
||||
history_manager
|
||||
.update_history_limit(limit)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,8 +162,10 @@ pub fn run() {
|
|||
TranscriptionManager::new(&app, model_manager.clone())
|
||||
.expect("Failed to initialize transcription manager"),
|
||||
);
|
||||
let history_manager =
|
||||
Arc::new(HistoryManager::new(&app).expect("Failed to initialize history manager"));
|
||||
let history_manager = Arc::new(
|
||||
HistoryManager::new(&app, settings.history_limit)
|
||||
.expect("Failed to initialize history manager"),
|
||||
);
|
||||
|
||||
// Add managers to Tauri's managed state
|
||||
app.manage(recording_manager.clone());
|
||||
|
|
@ -243,7 +245,8 @@ pub fn run() {
|
|||
commands::history::get_history_entries,
|
||||
commands::history::toggle_history_entry_saved,
|
||||
commands::history::get_audio_file_path,
|
||||
commands::history::delete_history_entry
|
||||
commands::history::delete_history_entry,
|
||||
commands::history::update_history_limit
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ use rusqlite::{params, Connection, OptionalExtension};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||
|
||||
use crate::audio_toolkit::save_wav_file;
|
||||
|
||||
const HISTORY_LIMIT: usize = 5;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HistoryEntry {
|
||||
pub id: i64,
|
||||
|
|
@ -22,15 +21,15 @@ pub struct HistoryEntry {
|
|||
pub transcription_text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HistoryManager {
|
||||
app_handle: AppHandle,
|
||||
recordings_dir: PathBuf,
|
||||
db_path: PathBuf,
|
||||
history_limit: AtomicUsize,
|
||||
}
|
||||
|
||||
impl HistoryManager {
|
||||
pub fn new(app: &App) -> Result<Self> {
|
||||
pub fn new(app: &App, history_limit: usize) -> Result<Self> {
|
||||
let app_handle = app.app_handle().clone();
|
||||
|
||||
// Create recordings directory in app data dir
|
||||
|
|
@ -48,6 +47,7 @@ impl HistoryManager {
|
|||
app_handle,
|
||||
recordings_dir,
|
||||
db_path,
|
||||
history_limit: AtomicUsize::new(history_limit),
|
||||
};
|
||||
|
||||
// Initialize database
|
||||
|
|
@ -99,6 +99,11 @@ impl HistoryManager {
|
|||
audio_samples: Vec<f32>,
|
||||
transcription_text: String,
|
||||
) -> Result<()> {
|
||||
// If history limit is 0, do not save at all.
|
||||
if self.history_limit.load(Ordering::Relaxed) == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let timestamp = Utc::now().timestamp();
|
||||
let file_name = format!("handy-{}.wav", timestamp);
|
||||
let title = self.format_timestamp_title(timestamp);
|
||||
|
|
@ -155,8 +160,9 @@ impl HistoryManager {
|
|||
entries.push(row?);
|
||||
}
|
||||
|
||||
if entries.len() > HISTORY_LIMIT {
|
||||
let entries_to_delete = &entries[HISTORY_LIMIT..];
|
||||
let limit = self.history_limit.load(Ordering::Relaxed);
|
||||
if entries.len() > limit {
|
||||
let entries_to_delete = &entries[limit..];
|
||||
|
||||
for (id, file_name) in entries_to_delete {
|
||||
// Delete database entry
|
||||
|
|
@ -242,26 +248,28 @@ impl HistoryManager {
|
|||
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"
|
||||
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")?,
|
||||
|
||||
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()?;
|
||||
|
||||
.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
|
||||
|
|
@ -273,7 +281,7 @@ impl HistoryManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Delete from database
|
||||
conn.execute(
|
||||
"DELETE FROM transcription_history WHERE id = ?1",
|
||||
|
|
@ -299,4 +307,10 @@ impl HistoryManager {
|
|||
format!("Recording {}", timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_history_limit(&self, new_limit: usize) -> Result<()> {
|
||||
self.history_limit.swap(new_limit, Ordering::Relaxed);
|
||||
self.cleanup_old_entries()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ pub struct AppSettings {
|
|||
pub model_unload_timeout: ModelUnloadTimeout,
|
||||
#[serde(default = "default_word_correction_threshold")]
|
||||
pub word_correction_threshold: f64,
|
||||
#[serde(default = "default_history_limit")]
|
||||
pub history_limit: usize,
|
||||
#[serde(default)]
|
||||
pub paste_method: PasteMethod,
|
||||
}
|
||||
|
|
@ -146,6 +148,10 @@ fn default_word_correction_threshold() -> f64 {
|
|||
0.18
|
||||
}
|
||||
|
||||
fn default_history_limit() -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
||||
|
||||
pub fn get_default_settings() -> AppSettings {
|
||||
|
|
@ -186,6 +192,7 @@ pub fn get_default_settings() -> AppSettings {
|
|||
custom_words: Vec::new(),
|
||||
model_unload_timeout: ModelUnloadTimeout::Never,
|
||||
word_correction_threshold: default_word_correction_threshold(),
|
||||
history_limit: default_history_limit(),
|
||||
paste_method: PasteMethod::default(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold";
|
||||
import { AppDataDirectory } from "./AppDataDirectory";
|
||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||
import { HistoryLimit } from "./HistoryLimit";
|
||||
|
||||
export const DebugSettings: React.FC = () => {
|
||||
return (
|
||||
|
|
@ -9,6 +10,7 @@ export const DebugSettings: React.FC = () => {
|
|||
<SettingsGroup title="Debug">
|
||||
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
||||
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
||||
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
48
src/components/settings/HistoryLimit.tsx
Normal file
48
src/components/settings/HistoryLimit.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from "react";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { Input } from "../ui/Input";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
|
||||
interface HistoryLimitProps {
|
||||
descriptionMode?: "tooltip" | "inline";
|
||||
grouped?: boolean;
|
||||
}
|
||||
|
||||
export const HistoryLimit: React.FC<HistoryLimitProps> = ({
|
||||
descriptionMode = "inline",
|
||||
grouped = false,
|
||||
}) => {
|
||||
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||
|
||||
const historyLimit = getSetting("history_limit") ?? 5;
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value, 10);
|
||||
if (!isNaN(value) && value >= 0) {
|
||||
updateSetting("history_limit", value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingContainer
|
||||
title="History Limit"
|
||||
description="Maximum number of transcription entries to keep in history"
|
||||
descriptionMode={descriptionMode}
|
||||
grouped={grouped}
|
||||
layout="horizontal"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000"
|
||||
value={historyLimit}
|
||||
onChange={handleChange}
|
||||
disabled={isUpdating("history_limit")}
|
||||
className="w-20"
|
||||
/>
|
||||
<span className="text-sm text-text">entries</span>
|
||||
</div>
|
||||
</SettingContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -18,3 +18,4 @@ export { CustomWords } from "./CustomWords";
|
|||
export { AppDataDirectory } from "./AppDataDirectory";
|
||||
export { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout";
|
||||
export { StartHidden } from "./StartHidden";
|
||||
export { HistoryLimit } from "./HistoryLimit";
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export const SettingsSchema = z.object({
|
|||
custom_words: z.array(z.string()).optional().default([]),
|
||||
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
|
||||
word_correction_threshold: z.number().optional().default(0.18),
|
||||
history_limit: z.number().optional().default(5),
|
||||
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
|
|||
overlay_position: "bottom",
|
||||
debug_mode: false,
|
||||
custom_words: [],
|
||||
history_limit: 5,
|
||||
};
|
||||
|
||||
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
||||
|
|
@ -224,6 +225,9 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
case "paste_method":
|
||||
await invoke("change_paste_method_setting", { method: value });
|
||||
break;
|
||||
case "history_limit":
|
||||
await invoke("update_history_limit", { limit: value });
|
||||
break;
|
||||
case "bindings":
|
||||
case "selected_model":
|
||||
break;
|
||||
|
|
@ -265,15 +269,15 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
set((state) => ({
|
||||
settings: state.settings
|
||||
? {
|
||||
...state.settings,
|
||||
bindings: {
|
||||
...state.settings.bindings,
|
||||
[id]: {
|
||||
...state.settings.bindings[id],
|
||||
current_binding: binding,
|
||||
},
|
||||
...state.settings,
|
||||
bindings: {
|
||||
...state.settings.bindings,
|
||||
[id]: {
|
||||
...state.settings.bindings[id],
|
||||
current_binding: binding,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
|
||||
|
|
@ -286,15 +290,15 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||
set((state) => ({
|
||||
settings: state.settings
|
||||
? {
|
||||
...state.settings,
|
||||
bindings: {
|
||||
...state.settings.bindings,
|
||||
[id]: {
|
||||
...state.settings.bindings[id],
|
||||
current_binding: originalBinding,
|
||||
},
|
||||
...state.settings,
|
||||
bindings: {
|
||||
...state.settings.bindings,
|
||||
[id]: {
|
||||
...state.settings.bindings[id],
|
||||
current_binding: originalBinding,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue