A few optimizations on startup (#182)
* Use an optimal release profile... load managers & hotkeys first, add a splash, simplify app handler * removed splashscreen, hopefully app boots fast enough anyhow * No need to load transcription model on start now that its loaded on demand & cleanup * Make an unloaded model status * tiny modification to unloading * update comment --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
a286fe5143
commit
93215dc097
11 changed files with 153 additions and 129 deletions
|
|
@ -71,3 +71,9 @@ tauri-plugin-autostart = "2"
|
|||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-single-instance = "2.3.2"
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
|
|
|||
|
|
@ -49,10 +49,101 @@ fn show_main_window(app: &AppHandle) {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Main window not found");
|
||||
eprintln!("Main window not found.");
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_core_logic(app_handle: &AppHandle) {
|
||||
// First, initialize the managers
|
||||
let recording_manager = Arc::new(
|
||||
AudioRecordingManager::new(app_handle).expect("Failed to initialize recording manager"),
|
||||
);
|
||||
let model_manager =
|
||||
Arc::new(ModelManager::new(app_handle).expect("Failed to initialize model manager"));
|
||||
let transcription_manager = Arc::new(
|
||||
TranscriptionManager::new(app_handle, model_manager.clone())
|
||||
.expect("Failed to initialize transcription manager"),
|
||||
);
|
||||
let history_manager =
|
||||
Arc::new(HistoryManager::new(app_handle).expect("Failed to initialize history manager"));
|
||||
|
||||
// Add managers to Tauri's managed state
|
||||
app_handle.manage(recording_manager.clone());
|
||||
app_handle.manage(model_manager.clone());
|
||||
app_handle.manage(transcription_manager.clone());
|
||||
app_handle.manage(history_manager.clone());
|
||||
|
||||
// Initialize the shortcuts
|
||||
shortcut::init_shortcuts(app_handle);
|
||||
|
||||
// Apply macOS Accessory policy if starting hidden
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let settings = settings::get_settings(app_handle);
|
||||
if settings.start_hidden {
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
}
|
||||
}
|
||||
// Get the current theme to set the appropriate initial icon
|
||||
let initial_theme = tray::get_current_theme(app_handle);
|
||||
|
||||
// Choose the appropriate initial icon based on theme
|
||||
let initial_icon_path = tray::get_icon_path(initial_theme, tray::TrayIconState::Idle);
|
||||
|
||||
let tray = TrayIconBuilder::new()
|
||||
.icon(
|
||||
Image::from_path(
|
||||
app_handle
|
||||
.path()
|
||||
.resolve(initial_icon_path, tauri::path::BaseDirectory::Resource)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.show_menu_on_left_click(true)
|
||||
.icon_as_template(true)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"settings" => {
|
||||
show_main_window(app);
|
||||
}
|
||||
"check_updates" => {
|
||||
show_main_window(app);
|
||||
let _ = app.emit("check-for-updates", ());
|
||||
}
|
||||
"cancel" => {
|
||||
use crate::utils::cancel_current_operation;
|
||||
|
||||
// Use centralized cancellation that handles all operations
|
||||
cancel_current_operation(app);
|
||||
}
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.build(app_handle)
|
||||
.unwrap();
|
||||
app_handle.manage(tray);
|
||||
|
||||
// Initialize tray menu with idle state
|
||||
utils::update_tray_menu(app_handle, &utils::TrayIconState::Idle);
|
||||
|
||||
// Get the autostart manager and configure based on user setting
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
let settings = settings::get_settings(&app_handle);
|
||||
|
||||
if settings.autostart_enabled {
|
||||
// Enable autostart if user has opted in
|
||||
let _ = autostart_manager.enable();
|
||||
} else {
|
||||
// Disable autostart if user has opted out
|
||||
let _ = autostart_manager.disable();
|
||||
}
|
||||
|
||||
// Create the recording overlay window (hidden by default)
|
||||
utils::create_recording_overlay(app_handle);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn trigger_update_check(app: AppHandle) -> Result<(), String> {
|
||||
app.emit("check-for-updates", ())
|
||||
|
|
@ -91,97 +182,18 @@ pub fn run() {
|
|||
))
|
||||
.manage(Mutex::new(ShortcutToggleStates::default()))
|
||||
.setup(move |app| {
|
||||
// Apply macOS Accessory policy early if starting hidden
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let settings = settings::get_settings(&app.handle());
|
||||
if settings.start_hidden {
|
||||
let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
let settings = settings::get_settings(&app.handle());
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
initialize_core_logic(&app_handle);
|
||||
|
||||
// Show main window only if not starting hidden
|
||||
if !settings.start_hidden {
|
||||
if let Some(main_window) = app_handle.get_webview_window("main") {
|
||||
main_window.show().unwrap();
|
||||
main_window.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
// Get the current theme to set the appropriate initial icon
|
||||
let initial_theme = tray::get_current_theme(&app.handle());
|
||||
|
||||
// Choose the appropriate initial icon based on theme
|
||||
let initial_icon_path = tray::get_icon_path(initial_theme, tray::TrayIconState::Idle);
|
||||
|
||||
let tray = TrayIconBuilder::new()
|
||||
.icon(Image::from_path(app.path().resolve(
|
||||
initial_icon_path,
|
||||
tauri::path::BaseDirectory::Resource,
|
||||
)?)?)
|
||||
.show_menu_on_left_click(true)
|
||||
.icon_as_template(true)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"settings" => {
|
||||
show_main_window(app);
|
||||
}
|
||||
"check_updates" => {
|
||||
show_main_window(app);
|
||||
let _ = app.emit("check-for-updates", ());
|
||||
}
|
||||
"cancel" => {
|
||||
use crate::utils::cancel_current_operation;
|
||||
|
||||
// Use centralized cancellation that handles all operations
|
||||
cancel_current_operation(app);
|
||||
}
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.build(app)?;
|
||||
app.manage(tray);
|
||||
|
||||
// Initialize tray menu with idle state
|
||||
utils::update_tray_menu(&app.handle(), &utils::TrayIconState::Idle);
|
||||
|
||||
// Get the autostart manager and configure based on user setting
|
||||
let autostart_manager = app.autolaunch();
|
||||
let settings = settings::get_settings(&app.handle());
|
||||
|
||||
if settings.autostart_enabled {
|
||||
// Enable autostart if user has opted in
|
||||
let _ = autostart_manager.enable();
|
||||
} else {
|
||||
// Disable autostart if user has opted out
|
||||
let _ = autostart_manager.disable();
|
||||
}
|
||||
|
||||
// Window is configured to start hidden to avoid flicker.
|
||||
// If user didn't choose Start Hidden, show it now.
|
||||
let settings = settings::get_settings(&app.handle());
|
||||
if settings.start_hidden {
|
||||
if let Some(main_window) = app.get_webview_window("main") {
|
||||
let _ = main_window.hide();
|
||||
}
|
||||
} else {
|
||||
show_main_window(&app.handle());
|
||||
}
|
||||
|
||||
let recording_manager = Arc::new(
|
||||
AudioRecordingManager::new(app).expect("Failed to initialize recording manager"),
|
||||
);
|
||||
let model_manager =
|
||||
Arc::new(ModelManager::new(&app).expect("Failed to initialize model manager"));
|
||||
let transcription_manager = Arc::new(
|
||||
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"));
|
||||
|
||||
// Add managers to Tauri's managed state
|
||||
app.manage(recording_manager.clone());
|
||||
app.manage(model_manager.clone());
|
||||
app.manage(transcription_manager.clone());
|
||||
app.manage(history_manager.clone());
|
||||
|
||||
// Create the recording overlay window (hidden by default)
|
||||
utils::create_recording_overlay(&app.handle());
|
||||
|
||||
shortcut::init_shortcuts(app);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::utils;
|
|||
use log::{debug, info};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use tauri::{App, Manager};
|
||||
use tauri::Manager;
|
||||
|
||||
const WHISPER_SAMPLE_RATE: usize = 16000;
|
||||
|
||||
|
|
@ -63,8 +63,8 @@ pub struct AudioRecordingManager {
|
|||
impl AudioRecordingManager {
|
||||
/* ---------- construction ------------------------------------------------ */
|
||||
|
||||
pub fn new(app: &App) -> Result<Self, anyhow::Error> {
|
||||
let settings = get_settings(&app.handle());
|
||||
pub fn new(app: &tauri::AppHandle) -> Result<Self, anyhow::Error> {
|
||||
let settings = get_settings(app);
|
||||
let mode = if settings.always_on_microphone {
|
||||
MicrophoneMode::AlwaysOn
|
||||
} else {
|
||||
|
|
@ -74,7 +74,7 @@ impl AudioRecordingManager {
|
|||
let manager = Self {
|
||||
state: Arc::new(Mutex::new(RecordingState::Idle)),
|
||||
mode: Arc::new(Mutex::new(mode.clone())),
|
||||
app_handle: app.handle().clone(),
|
||||
app_handle: app.clone(),
|
||||
|
||||
recorder: Arc::new(Mutex::new(None)),
|
||||
is_open: Arc::new(Mutex::new(false)),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use rusqlite::{params, Connection, OptionalExtension};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||
|
||||
use crate::audio_toolkit::save_wav_file;
|
||||
|
|
@ -27,11 +27,9 @@ pub struct HistoryManager {
|
|||
}
|
||||
|
||||
impl HistoryManager {
|
||||
pub fn new(app: &App) -> Result<Self> {
|
||||
let app_handle = app.app_handle().clone();
|
||||
|
||||
pub fn new(app_handle: &AppHandle) -> Result<Self> {
|
||||
// Create recordings directory in app data dir
|
||||
let app_data_dir = app.path().app_data_dir()?;
|
||||
let app_data_dir = app_handle.path().app_data_dir()?;
|
||||
let recordings_dir = app_data_dir.join("recordings");
|
||||
let db_path = app_data_dir.join("history.db");
|
||||
|
||||
|
|
@ -42,7 +40,7 @@ impl HistoryManager {
|
|||
}
|
||||
|
||||
let manager = Self {
|
||||
app_handle,
|
||||
app_handle: app_handle.clone(),
|
||||
recordings_dir,
|
||||
db_path,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::io::Write;
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use tar::Archive;
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum EngineType {
|
||||
|
|
@ -48,11 +48,9 @@ pub struct ModelManager {
|
|||
}
|
||||
|
||||
impl ModelManager {
|
||||
pub fn new(app: &App) -> Result<Self> {
|
||||
let app_handle = app.app_handle().clone();
|
||||
|
||||
pub fn new(app_handle: &AppHandle) -> Result<Self> {
|
||||
// Create models directory in app data
|
||||
let models_dir = app
|
||||
let models_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get app data dir: {}", e))?
|
||||
|
|
@ -152,7 +150,7 @@ impl ModelManager {
|
|||
);
|
||||
|
||||
let manager = Self {
|
||||
app_handle,
|
||||
app_handle: app_handle.clone(),
|
||||
models_dir,
|
||||
available_models: Mutex::new(available_models),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use transcribe_rs::{
|
||||
engines::{
|
||||
parakeet::{
|
||||
|
|
@ -46,9 +46,7 @@ pub struct TranscriptionManager {
|
|||
}
|
||||
|
||||
impl TranscriptionManager {
|
||||
pub fn new(app: &App, model_manager: Arc<ModelManager>) -> Result<Self> {
|
||||
let app_handle = app.app_handle().clone();
|
||||
|
||||
pub fn new(app_handle: &AppHandle, model_manager: Arc<ModelManager>) -> Result<Self> {
|
||||
let manager = Self {
|
||||
engine: Arc::new(Mutex::new(None)),
|
||||
model_manager,
|
||||
|
|
@ -105,7 +103,7 @@ impl TranscriptionManager {
|
|||
let _ = app_handle_cloned.emit(
|
||||
"model-state-changed",
|
||||
ModelStateEvent {
|
||||
event_type: "unloaded_due_to_idle".to_string(),
|
||||
event_type: "unloaded".to_string(),
|
||||
model_id: None,
|
||||
model_name: None,
|
||||
error: None,
|
||||
|
|
@ -126,10 +124,6 @@ impl TranscriptionManager {
|
|||
*manager.watcher_handle.lock().unwrap() = Some(handle);
|
||||
}
|
||||
|
||||
// Try to load the default model from settings, but don't fail if no models are available
|
||||
let settings = get_settings(&app_handle);
|
||||
let _ = manager.load_model(&settings.selected_model);
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +155,7 @@ impl TranscriptionManager {
|
|||
let _ = self.app_handle.emit(
|
||||
"model-state-changed",
|
||||
ModelStateEvent {
|
||||
event_type: "unloaded_manually".to_string(),
|
||||
event_type: "unloaded".to_string(),
|
||||
model_id: None,
|
||||
model_name: None,
|
||||
error: None,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tauri::{App, AppHandle};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
|
@ -247,7 +247,7 @@ pub fn get_default_settings() -> AppSettings {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_or_create_app_settings(app: &App) -> AppSettings {
|
||||
pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings {
|
||||
// Initialize store
|
||||
let store = app
|
||||
.store(SETTINGS_STORE_PATH)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
use serde::Serialize;
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||
use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
|
||||
use crate::actions::ACTION_MAP;
|
||||
use crate::settings::ShortcutBinding;
|
||||
use crate::settings::{self, get_settings, OverlayPosition, PasteMethod, SoundTheme};
|
||||
use crate::ManagedToggleState;
|
||||
|
||||
pub fn init_shortcuts(app: &App) {
|
||||
pub fn init_shortcuts(app: &AppHandle) {
|
||||
let settings = settings::load_or_create_app_settings(app);
|
||||
|
||||
// Register shortcuts with the bindings from settings
|
||||
for (_id, binding) in settings.bindings {
|
||||
// Pass app.handle() which is &AppHandle
|
||||
if let Err(e) = _register_shortcut(app.handle(), binding) {
|
||||
if let Err(e) = _register_shortcut(app, binding) {
|
||||
eprintln!("Failed to register shortcut {} during init: {}", _id, e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@
|
|||
"macOSPrivateApi": true,
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Handy",
|
||||
"width": 680,
|
||||
"height": 510,
|
||||
"minWidth": 680,
|
||||
"minHeight": 510,
|
||||
"resizable": true,
|
||||
"maximizable": false
|
||||
"maximizable": false,
|
||||
"visible": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,14 @@ interface DownloadProgress {
|
|||
percentage: number;
|
||||
}
|
||||
|
||||
type ModelStatus = "ready" | "loading" | "downloading" | "extracting" | "error" | "none";
|
||||
type ModelStatus =
|
||||
| "ready"
|
||||
| "loading"
|
||||
| "downloading"
|
||||
| "extracting"
|
||||
| "error"
|
||||
| "unloaded"
|
||||
| "none";
|
||||
|
||||
interface DownloadStats {
|
||||
startTime: number;
|
||||
|
|
@ -36,7 +43,7 @@ interface ModelSelectorProps {
|
|||
const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
||||
const [currentModelId, setCurrentModelId] = useState<string>("");
|
||||
const [modelStatus, setModelStatus] = useState<ModelStatus>("loading");
|
||||
const [modelStatus, setModelStatus] = useState<ModelStatus>("unloaded");
|
||||
const [modelError, setModelError] = useState<string | null>(null);
|
||||
const [modelDownloadProgress, setModelDownloadProgress] = useState<
|
||||
Map<string, DownloadProgress>
|
||||
|
|
@ -75,6 +82,10 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
setModelStatus("error");
|
||||
setModelError(error || "Failed to load model");
|
||||
break;
|
||||
case "unloaded":
|
||||
setModelStatus("unloaded");
|
||||
setModelError(null);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -247,7 +258,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
if (transcriptionStatus === current) {
|
||||
setModelStatus("ready");
|
||||
} else {
|
||||
setModelStatus("loading");
|
||||
setModelStatus("unloaded");
|
||||
}
|
||||
} else {
|
||||
setModelStatus("none");
|
||||
|
|
@ -324,10 +335,12 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
|
|||
return currentModel ? `Extracting ${currentModel.name}...` : "Extracting...";
|
||||
case "error":
|
||||
return modelError || "Model Error";
|
||||
case "unloaded":
|
||||
return currentModel?.name || "Model Unloaded";
|
||||
case "none":
|
||||
return "No Model - Download Required";
|
||||
default:
|
||||
return currentModel?.name || "Select Model";
|
||||
return currentModel?.name || "Model Unloaded";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type ModelStatus =
|
|||
| "downloading"
|
||||
| "extracting"
|
||||
| "error"
|
||||
| "unloaded"
|
||||
| "none";
|
||||
|
||||
interface ModelStatusButtonProps {
|
||||
|
|
@ -35,6 +36,8 @@ const ModelStatusButton: React.FC<ModelStatusButtonProps> = ({
|
|||
return "bg-orange-400 animate-pulse";
|
||||
case "error":
|
||||
return "bg-red-400";
|
||||
case "unloaded":
|
||||
return "bg-mid-gray/60";
|
||||
case "none":
|
||||
return "bg-red-400";
|
||||
default:
|
||||
|
|
|
|||
Loading…
Reference in a new issue