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,
|
id: i64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
history_manager
|
history_manager
|
||||||
.as_ref()
|
|
||||||
.delete_entry(id)
|
.delete_entry(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.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())
|
TranscriptionManager::new(&app, model_manager.clone())
|
||||||
.expect("Failed to initialize transcription manager"),
|
.expect("Failed to initialize transcription manager"),
|
||||||
);
|
);
|
||||||
let history_manager =
|
let history_manager = Arc::new(
|
||||||
Arc::new(HistoryManager::new(&app).expect("Failed to initialize history manager"));
|
HistoryManager::new(&app, settings.history_limit)
|
||||||
|
.expect("Failed to initialize history manager"),
|
||||||
|
);
|
||||||
|
|
||||||
// Add managers to Tauri's managed state
|
// Add managers to Tauri's managed state
|
||||||
app.manage(recording_manager.clone());
|
app.manage(recording_manager.clone());
|
||||||
|
|
@ -243,7 +245,8 @@ pub fn run() {
|
||||||
commands::history::get_history_entries,
|
commands::history::get_history_entries,
|
||||||
commands::history::toggle_history_entry_saved,
|
commands::history::toggle_history_entry_saved,
|
||||||
commands::history::get_audio_file_path,
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,12 @@ use rusqlite::{params, Connection, OptionalExtension};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use tauri::{App, AppHandle, Emitter, Manager};
|
use tauri::{App, AppHandle, Emitter, Manager};
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
|
|
||||||
use crate::audio_toolkit::save_wav_file;
|
use crate::audio_toolkit::save_wav_file;
|
||||||
|
|
||||||
const HISTORY_LIMIT: usize = 5;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct HistoryEntry {
|
pub struct HistoryEntry {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
|
@ -22,15 +21,15 @@ pub struct HistoryEntry {
|
||||||
pub transcription_text: String,
|
pub transcription_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct HistoryManager {
|
pub struct HistoryManager {
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
recordings_dir: PathBuf,
|
recordings_dir: PathBuf,
|
||||||
db_path: PathBuf,
|
db_path: PathBuf,
|
||||||
|
history_limit: AtomicUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryManager {
|
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();
|
let app_handle = app.app_handle().clone();
|
||||||
|
|
||||||
// Create recordings directory in app data dir
|
// Create recordings directory in app data dir
|
||||||
|
|
@ -48,6 +47,7 @@ impl HistoryManager {
|
||||||
app_handle,
|
app_handle,
|
||||||
recordings_dir,
|
recordings_dir,
|
||||||
db_path,
|
db_path,
|
||||||
|
history_limit: AtomicUsize::new(history_limit),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
|
|
@ -99,6 +99,11 @@ impl HistoryManager {
|
||||||
audio_samples: Vec<f32>,
|
audio_samples: Vec<f32>,
|
||||||
transcription_text: String,
|
transcription_text: String,
|
||||||
) -> Result<()> {
|
) -> 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 timestamp = Utc::now().timestamp();
|
||||||
let file_name = format!("handy-{}.wav", timestamp);
|
let file_name = format!("handy-{}.wav", timestamp);
|
||||||
let title = self.format_timestamp_title(timestamp);
|
let title = self.format_timestamp_title(timestamp);
|
||||||
|
|
@ -155,8 +160,9 @@ impl HistoryManager {
|
||||||
entries.push(row?);
|
entries.push(row?);
|
||||||
}
|
}
|
||||||
|
|
||||||
if entries.len() > HISTORY_LIMIT {
|
let limit = self.history_limit.load(Ordering::Relaxed);
|
||||||
let entries_to_delete = &entries[HISTORY_LIMIT..];
|
if entries.len() > limit {
|
||||||
|
let entries_to_delete = &entries[limit..];
|
||||||
|
|
||||||
for (id, file_name) in entries_to_delete {
|
for (id, file_name) in entries_to_delete {
|
||||||
// Delete database entry
|
// Delete database entry
|
||||||
|
|
@ -242,19 +248,21 @@ impl HistoryManager {
|
||||||
let conn = self.get_connection()?;
|
let conn = self.get_connection()?;
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, file_name, timestamp, saved, title, transcription_text
|
"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| {
|
let entry = stmt
|
||||||
Ok(HistoryEntry {
|
.query_row([id], |row| {
|
||||||
id: row.get("id")?,
|
Ok(HistoryEntry {
|
||||||
file_name: row.get("file_name")?,
|
id: row.get("id")?,
|
||||||
timestamp: row.get("timestamp")?,
|
file_name: row.get("file_name")?,
|
||||||
saved: row.get("saved")?,
|
timestamp: row.get("timestamp")?,
|
||||||
title: row.get("title")?,
|
saved: row.get("saved")?,
|
||||||
transcription_text: row.get("transcription_text")?,
|
title: row.get("title")?,
|
||||||
|
transcription_text: row.get("transcription_text")?,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}).optional()?;
|
.optional()?;
|
||||||
|
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
}
|
}
|
||||||
|
|
@ -299,4 +307,10 @@ impl HistoryManager {
|
||||||
format!("Recording {}", timestamp)
|
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,
|
pub model_unload_timeout: ModelUnloadTimeout,
|
||||||
#[serde(default = "default_word_correction_threshold")]
|
#[serde(default = "default_word_correction_threshold")]
|
||||||
pub word_correction_threshold: f64,
|
pub word_correction_threshold: f64,
|
||||||
|
#[serde(default = "default_history_limit")]
|
||||||
|
pub history_limit: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub paste_method: PasteMethod,
|
pub paste_method: PasteMethod,
|
||||||
}
|
}
|
||||||
|
|
@ -146,6 +148,10 @@ fn default_word_correction_threshold() -> f64 {
|
||||||
0.18
|
0.18
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_history_limit() -> usize {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
|
||||||
|
|
||||||
pub fn get_default_settings() -> AppSettings {
|
pub fn get_default_settings() -> AppSettings {
|
||||||
|
|
@ -186,6 +192,7 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
custom_words: Vec::new(),
|
custom_words: Vec::new(),
|
||||||
model_unload_timeout: ModelUnloadTimeout::Never,
|
model_unload_timeout: ModelUnloadTimeout::Never,
|
||||||
word_correction_threshold: default_word_correction_threshold(),
|
word_correction_threshold: default_word_correction_threshold(),
|
||||||
|
history_limit: default_history_limit(),
|
||||||
paste_method: PasteMethod::default(),
|
paste_method: PasteMethod::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold";
|
import { WordCorrectionThreshold } from "./debug/WordCorrectionThreshold";
|
||||||
import { AppDataDirectory } from "./AppDataDirectory";
|
import { AppDataDirectory } from "./AppDataDirectory";
|
||||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||||
|
import { HistoryLimit } from "./HistoryLimit";
|
||||||
|
|
||||||
export const DebugSettings: React.FC = () => {
|
export const DebugSettings: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -9,6 +10,7 @@ export const DebugSettings: React.FC = () => {
|
||||||
<SettingsGroup title="Debug">
|
<SettingsGroup title="Debug">
|
||||||
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
||||||
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
||||||
|
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
</div>
|
</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 { AppDataDirectory } from "./AppDataDirectory";
|
||||||
export { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout";
|
export { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout";
|
||||||
export { StartHidden } from "./StartHidden";
|
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([]),
|
custom_words: z.array(z.string()).optional().default([]),
|
||||||
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
|
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
|
||||||
word_correction_threshold: z.number().optional().default(0.18),
|
word_correction_threshold: z.number().optional().default(0.18),
|
||||||
|
history_limit: z.number().optional().default(5),
|
||||||
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
|
||||||
overlay_position: "bottom",
|
overlay_position: "bottom",
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
custom_words: [],
|
custom_words: [],
|
||||||
|
history_limit: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
||||||
|
|
@ -224,6 +225,9 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
case "paste_method":
|
case "paste_method":
|
||||||
await invoke("change_paste_method_setting", { method: value });
|
await invoke("change_paste_method_setting", { method: value });
|
||||||
break;
|
break;
|
||||||
|
case "history_limit":
|
||||||
|
await invoke("update_history_limit", { limit: value });
|
||||||
|
break;
|
||||||
case "bindings":
|
case "bindings":
|
||||||
case "selected_model":
|
case "selected_model":
|
||||||
break;
|
break;
|
||||||
|
|
@ -265,15 +269,15 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
settings: state.settings
|
settings: state.settings
|
||||||
? {
|
? {
|
||||||
...state.settings,
|
...state.settings,
|
||||||
bindings: {
|
bindings: {
|
||||||
...state.settings.bindings,
|
...state.settings.bindings,
|
||||||
[id]: {
|
[id]: {
|
||||||
...state.settings.bindings[id],
|
...state.settings.bindings[id],
|
||||||
current_binding: binding,
|
current_binding: binding,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -286,15 +290,15 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
settings: state.settings
|
settings: state.settings
|
||||||
? {
|
? {
|
||||||
...state.settings,
|
...state.settings,
|
||||||
bindings: {
|
bindings: {
|
||||||
...state.settings.bindings,
|
...state.settings.bindings,
|
||||||
[id]: {
|
[id]: {
|
||||||
...state.settings.bindings[id],
|
...state.settings.bindings[id],
|
||||||
current_binding: originalBinding,
|
current_binding: originalBinding,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue