From fa7e812166e3a3bcfc5709f4d4d64781cae8ce6b Mon Sep 17 00:00:00 2001 From: jake Date: Sat, 21 Mar 2026 10:41:44 +0000 Subject: [PATCH] =?UTF-8?q?agent:=20rust=20=E2=80=94=20add=20preferences?= =?UTF-8?q?=20webview=20injection=20for=20zero-flash=20hydration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/Cargo.toml | 1 + src-tauri/src/commands/windows.rs | 32 ++++++--- src-tauri/src/lib.rs | 109 +++++++++++++++++++++++++----- 3 files changed, 118 insertions(+), 24 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2d93ee5..71b30a8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,3 +35,4 @@ serde_json = "1" tokio = { version = "1", features = ["rt", "sync"] } arboard = "3.6.1" tauri-plugin-mcp = "0.7.1" +sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] } diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index 234d380..9c3a40e 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -1,5 +1,7 @@ use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; +use crate::PreferencesScript; + /// Open a floating always-on-top task window. #[tauri::command] pub async fn open_task_window( @@ -13,7 +15,7 @@ pub async fn open_task_window( return Ok(()); } - WebviewWindowBuilder::new( + let mut builder = WebviewWindowBuilder::new( &app, "tasks-float", WebviewUrl::App("/float".into()), @@ -23,9 +25,16 @@ pub async fn open_task_window( .min_inner_size(400.0, 400.0) .always_on_top(true) .decorations(false) - .resizable(true) - .build() - .map_err(|e| e.to_string())?; + .resizable(true); + + // Inject preferences before Svelte mounts + if let Some(script) = app.try_state::() { + if !script.0.is_empty() { + builder = builder.initialization_script(&script.0); + } + } + + builder.build().map_err(|e| e.to_string())?; Ok(()) } @@ -41,7 +50,7 @@ pub async fn open_viewer_window( return Ok(()); } - WebviewWindowBuilder::new( + let mut builder = WebviewWindowBuilder::new( &app, "transcript-viewer", WebviewUrl::App("/viewer".into()), @@ -50,9 +59,16 @@ pub async fn open_viewer_window( .inner_size(600.0, 700.0) .min_inner_size(450.0, 500.0) .decorations(false) - .resizable(true) - .build() - .map_err(|e| e.to_string())?; + .resizable(true); + + // Inject preferences before Svelte mounts + if let Some(script) = app.try_state::() { + if !script.0.is_empty() { + builder = builder.initialization_script(&script.0); + } + } + + builder.build().map_err(|e| e.to_string())?; Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c1c486c..df6af98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,15 +3,67 @@ mod tray; use std::sync::Arc; +use sqlx::SqlitePool; use tauri::Manager; use kon_core::types::EngineName; +use kon_storage::{database_path, get_setting, init as init_db, set_setting}; use kon_transcription::LocalEngine; -/// Shared app state holding the transcription engines. +/// Shared app state holding the transcription engines and database pool. pub struct AppState { pub whisper_engine: Arc, pub parakeet_engine: Arc, + pub db: SqlitePool, +} + +/// Holds the preferences init script for injection into secondary windows. +pub struct PreferencesScript(pub String); + +/// Build a JavaScript snippet that applies saved preferences to the DOM +/// before Svelte mounts, preventing any flash of unstyled content. +fn build_preferences_script(prefs_json: Option) -> String { + let json = prefs_json.unwrap_or_default(); + if json.is_empty() { + return String::new(); + } + // Serialise the JSON string as a JS string literal for safe embedding + let js_str = serde_json::to_string(&json).unwrap_or_else(|_| "\"\"".to_string()); + format!( + r#"(function() {{ + try {{ + var p = JSON.parse({js_str}); + var el = document.documentElement; + if (p.theme === 'system') {{ + el.dataset.theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + }} else if (p.theme) {{ + el.dataset.theme = p.theme; + }} + if (p.zone && p.zone !== 'default') el.dataset.zone = p.zone; + if (p.accessibility) {{ + var a = p.accessibility; + var fonts = {{ lexend: "'Lexend', system-ui, sans-serif", atkinson: "'Atkinson Hyperlegible Next', system-ui, sans-serif", opendyslexic: "'OpenDyslexic', system-ui, sans-serif" }}; + if (a.fontFamily) {{ el.style.setProperty('--font-family-body', fonts[a.fontFamily] || fonts.lexend); el.dataset.fontFamily = a.fontFamily; }} + if (a.fontSize) el.style.setProperty('--font-size-body', a.fontSize + 'px'); + if (a.letterSpacing != null) el.style.setProperty('--letter-spacing-body', a.letterSpacing + 'em'); + if (a.lineHeight) el.style.setProperty('--line-height-body', String(a.lineHeight)); + if (a.bionicReading) el.dataset.bionicReading = 'true'; + if (a.reduceMotion === 'on' || (a.reduceMotion === 'system' && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) el.dataset.reduceMotion = 'true'; + }} + }} catch (e) {{ console.warn('Preferences injection failed:', e); }} +}})();"# + ) +} + +/// Save preferences JSON to the SQLite settings table. +#[tauri::command] +async fn save_preferences( + state: tauri::State<'_, AppState>, + preferences: String, +) -> Result<(), String> { + set_setting(&state.db, "kon_preferences", &preferences) + .await + .map_err(|e| e.to_string()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -20,35 +72,60 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) - .manage(AppState { - whisper_engine: Arc::new(LocalEngine::new( - EngineName::new("whisper"), - )), - parakeet_engine: Arc::new(LocalEngine::new( - EngineName::new("parakeet"), - )), - }) .setup(|app| { - if let Err(e) = tray::setup(app) { - eprintln!("Failed to setup tray: {e}"); - } + // Initialise database (blocking in setup — runs once at startup) + let db_path = database_path(); + let db = tauri::async_runtime::block_on(async { + init_db(&db_path).await + }) + .map_err(|e| Box::new(e) as Box)?; - // Close-to-tray: hide window instead of exiting + // Load saved preferences for webview injection + let prefs_json = tauri::async_runtime::block_on(async { + get_setting(&db, "kon_preferences").await.unwrap_or(None) + }); + let init_script = build_preferences_script(prefs_json); + + // Apply preferences to the main window (defined in tauri.conf.json) if let Some(main_window) = app.get_webview_window("main") { + if !init_script.is_empty() { + // Tauri's WebviewWindow.eval() is the standard API for + // running JS in a webview — not the JS eval() function + let _ = main_window.eval(&init_script); + } + + // Close-to-tray: hide window instead of exiting let win = main_window.clone(); main_window.on_window_event(move |event| { - if let tauri::WindowEvent::CloseRequested { api, .. } = - event - { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); let _ = win.hide(); } }); } + // Store init script for secondary windows (float, viewer) + app.manage(PreferencesScript(init_script)); + + app.manage(AppState { + whisper_engine: Arc::new(LocalEngine::new( + EngineName::new("whisper"), + )), + parakeet_engine: Arc::new(LocalEngine::new( + EngineName::new("parakeet"), + )), + db, + }); + + if let Err(e) = tray::setup(app) { + eprintln!("Failed to setup tray: {e}"); + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ + // Preferences + save_preferences, // Whisper model management commands::models::download_model, commands::models::check_model,