agent: rust — add preferences webview injection for zero-flash hydration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,3 +35,4 @@ serde_json = "1"
|
|||||||
tokio = { version = "1", features = ["rt", "sync"] }
|
tokio = { version = "1", features = ["rt", "sync"] }
|
||||||
arboard = "3.6.1"
|
arboard = "3.6.1"
|
||||||
tauri-plugin-mcp = "0.7.1"
|
tauri-plugin-mcp = "0.7.1"
|
||||||
|
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
|
use crate::PreferencesScript;
|
||||||
|
|
||||||
/// Open a floating always-on-top task window.
|
/// Open a floating always-on-top task window.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn open_task_window(
|
pub async fn open_task_window(
|
||||||
@@ -13,7 +15,7 @@ pub async fn open_task_window(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
WebviewWindowBuilder::new(
|
let mut builder = WebviewWindowBuilder::new(
|
||||||
&app,
|
&app,
|
||||||
"tasks-float",
|
"tasks-float",
|
||||||
WebviewUrl::App("/float".into()),
|
WebviewUrl::App("/float".into()),
|
||||||
@@ -23,9 +25,16 @@ pub async fn open_task_window(
|
|||||||
.min_inner_size(400.0, 400.0)
|
.min_inner_size(400.0, 400.0)
|
||||||
.always_on_top(true)
|
.always_on_top(true)
|
||||||
.decorations(false)
|
.decorations(false)
|
||||||
.resizable(true)
|
.resizable(true);
|
||||||
.build()
|
|
||||||
.map_err(|e| e.to_string())?;
|
// Inject preferences before Svelte mounts
|
||||||
|
if let Some(script) = app.try_state::<PreferencesScript>() {
|
||||||
|
if !script.0.is_empty() {
|
||||||
|
builder = builder.initialization_script(&script.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -41,7 +50,7 @@ pub async fn open_viewer_window(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
WebviewWindowBuilder::new(
|
let mut builder = WebviewWindowBuilder::new(
|
||||||
&app,
|
&app,
|
||||||
"transcript-viewer",
|
"transcript-viewer",
|
||||||
WebviewUrl::App("/viewer".into()),
|
WebviewUrl::App("/viewer".into()),
|
||||||
@@ -50,9 +59,16 @@ pub async fn open_viewer_window(
|
|||||||
.inner_size(600.0, 700.0)
|
.inner_size(600.0, 700.0)
|
||||||
.min_inner_size(450.0, 500.0)
|
.min_inner_size(450.0, 500.0)
|
||||||
.decorations(false)
|
.decorations(false)
|
||||||
.resizable(true)
|
.resizable(true);
|
||||||
.build()
|
|
||||||
.map_err(|e| e.to_string())?;
|
// Inject preferences before Svelte mounts
|
||||||
|
if let Some(script) = app.try_state::<PreferencesScript>() {
|
||||||
|
if !script.0.is_empty() {
|
||||||
|
builder = builder.initialization_script(&script.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,67 @@ mod tray;
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sqlx::SqlitePool;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
use kon_core::types::EngineName;
|
use kon_core::types::EngineName;
|
||||||
|
use kon_storage::{database_path, get_setting, init as init_db, set_setting};
|
||||||
use kon_transcription::LocalEngine;
|
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 struct AppState {
|
||||||
pub whisper_engine: Arc<LocalEngine>,
|
pub whisper_engine: Arc<LocalEngine>,
|
||||||
pub parakeet_engine: Arc<LocalEngine>,
|
pub parakeet_engine: Arc<LocalEngine>,
|
||||||
|
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>) -> 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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -20,35 +72,60 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
.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| {
|
.setup(|app| {
|
||||||
if let Err(e) = tray::setup(app) {
|
// Initialise database (blocking in setup — runs once at startup)
|
||||||
eprintln!("Failed to setup tray: {e}");
|
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<dyn std::error::Error>)?;
|
||||||
|
|
||||||
// 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 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();
|
let win = main_window.clone();
|
||||||
main_window.on_window_event(move |event| {
|
main_window.on_window_event(move |event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { api, .. } =
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
event
|
|
||||||
{
|
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
let _ = win.hide();
|
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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
// Preferences
|
||||||
|
save_preferences,
|
||||||
// Whisper model management
|
// Whisper model management
|
||||||
commands::models::download_model,
|
commands::models::download_model,
|
||||||
commands::models::check_model,
|
commands::models::check_model,
|
||||||
|
|||||||
Reference in New Issue
Block a user