diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 07a81cb..041fb4d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kjvstudy" -version = "1.0.0" +version = "0.1.0" description = "KJV Study - Offline Bible Study App" authors = ["Kenneth Reitz"] license = "MIT" @@ -11,12 +11,13 @@ edition = "2021" tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["webview-data-url"] } tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["process", "time"] } reqwest = { version = "0.12", features = ["blocking"] } +urlencoding = "2" [features] default = ["custom-protocol"] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5a23084..ad200d3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,20 +4,79 @@ use std::process::{Child, Command, Stdio}; use std::sync::Mutex; use std::time::Duration; -use tauri::{Manager, RunEvent, WebviewUrl, WebviewWindowBuilder}; +use tauri::{ + Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, + menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}, + Emitter, +}; struct ServerProcess(Mutex>); const SERVER_URL: &str = "http://127.0.0.1:31102"; +const APP_VERSION: &str = "0.1.0"; + +// Loading screen HTML +const LOADING_HTML: &str = r#" + + + + + + + + +

KJV STUDY

+
+

Starting server...

+ + +"#; fn start_server() -> Option { - // Get the current working directory (project root in dev) let working_dir = std::env::current_dir().ok()?; println!("Starting KJV Study server..."); println!("Working directory: {:?}", working_dir); - // Try uv first (preferred), then fall back to python3 let child = Command::new("uv") .args([ "run", "uvicorn", @@ -65,20 +124,183 @@ fn wait_for_server(max_attempts: u32) -> bool { println!("Server is ready!"); return true; } - Ok(response) => { - println!("Server responded with status: {}", response.status()); - } - Err(e) => { - println!("Connection error: {}", e); + _ => { + std::thread::sleep(Duration::from_millis(500)); } } - std::thread::sleep(Duration::from_millis(500)); } println!("Server failed to start after {} attempts", max_attempts); false } +fn create_menu(app: &tauri::AppHandle) -> tauri::Result> { + // App menu (macOS) + let about = MenuItem::with_id(app, "about", "About KJV Study", true, None::<&str>)?; + let quit = PredefinedMenuItem::quit(app, Some("Quit KJV Study"))?; + let app_menu = Submenu::with_items( + app, + "KJV Study", + true, + &[&about, &PredefinedMenuItem::separator(app)?, &quit], + )?; + + // File menu + let close = PredefinedMenuItem::close_window(app, Some("Close Window"))?; + let file_menu = Submenu::with_items(app, "File", true, &[&close])?; + + // Edit menu + let edit_menu = Submenu::with_items( + app, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app, None)?, + &PredefinedMenuItem::redo(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::cut(app, None)?, + &PredefinedMenuItem::copy(app, None)?, + &PredefinedMenuItem::paste(app, None)?, + &PredefinedMenuItem::select_all(app, None)?, + ], + )?; + + // Navigate menu + let nav_home = MenuItem::with_id(app, "nav_home", "Home", true, Some("CmdOrCtrl+Shift+H"))?; + let nav_books = MenuItem::with_id(app, "nav_books", "Books", true, Some("CmdOrCtrl+B"))?; + let nav_search = MenuItem::with_id(app, "nav_search", "Search", true, Some("CmdOrCtrl+K"))?; + let nav_votd = MenuItem::with_id(app, "nav_votd", "Verse of the Day", true, Some("CmdOrCtrl+D"))?; + let nav_random = MenuItem::with_id(app, "nav_random", "Random Verse", true, Some("CmdOrCtrl+R"))?; + + let navigate_menu = Submenu::with_items( + app, + "Navigate", + true, + &[ + &nav_home, + &nav_books, + &PredefinedMenuItem::separator(app)?, + &nav_search, + &nav_votd, + &nav_random, + ], + )?; + + // Study menu + let study_guides = MenuItem::with_id(app, "study_guides", "Study Guides", true, None::<&str>)?; + let topics = MenuItem::with_id(app, "topics", "Topics", true, None::<&str>)?; + let stories = MenuItem::with_id(app, "stories", "Bible Stories", true, None::<&str>)?; + let strongs = MenuItem::with_id(app, "strongs", "Strong's Concordance", true, None::<&str>)?; + let interlinear = MenuItem::with_id(app, "interlinear", "Interlinear", true, None::<&str>)?; + let reading_plans = MenuItem::with_id(app, "reading_plans", "Reading Plans", true, None::<&str>)?; + + let study_menu = Submenu::with_items( + app, + "Study", + true, + &[ + &study_guides, + &topics, + &stories, + &PredefinedMenuItem::separator(app)?, + &strongs, + &interlinear, + &PredefinedMenuItem::separator(app)?, + &reading_plans, + ], + )?; + + // View menu + let reload = MenuItem::with_id(app, "reload", "Reload", true, Some("CmdOrCtrl+Shift+R"))?; + let back = MenuItem::with_id(app, "back", "Back", true, Some("CmdOrCtrl+["))?; + let forward = MenuItem::with_id(app, "forward", "Forward", true, Some("CmdOrCtrl+]"))?; + let fullscreen = PredefinedMenuItem::fullscreen(app, None)?; + + let view_menu = Submenu::with_items( + app, + "View", + true, + &[ + &back, + &forward, + &reload, + &PredefinedMenuItem::separator(app)?, + &fullscreen, + ], + )?; + + // Window menu + let minimize = PredefinedMenuItem::minimize(app, None)?; + let zoom = PredefinedMenuItem::maximize(app, Some("Zoom"))?; + + let window_menu = Submenu::with_items( + app, + "Window", + true, + &[&minimize, &zoom], + )?; + + // Help menu + let website = MenuItem::with_id(app, "website", "Visit kjvstudy.org", true, None::<&str>)?; + let help_menu = Submenu::with_items(app, "Help", true, &[&website])?; + + Menu::with_items( + app, + &[&app_menu, &file_menu, &edit_menu, &navigate_menu, &study_menu, &view_menu, &window_menu, &help_menu], + ) +} + +fn show_about_dialog(app: &tauri::AppHandle) { + let about_html = format!(r#" + + + + + + + +

KJV Study

+

Version {}

+

+ A comprehensive offline Bible study application featuring the 1769 Cambridge King James Version + with commentary, cross-references, and original language tools. +

+ + + + "#, APP_VERSION); + + let about_url = format!("data:text/html,{}", urlencoding::encode(&about_html)); + + if let Ok(_) = WebviewWindowBuilder::new( + app, + "about", + WebviewUrl::External(about_url.parse().unwrap()) + ) + .title("About KJV Study") + .inner_size(350.0, 320.0) + .resizable(false) + .minimizable(false) + .maximizable(false) + .center() + .build() { + println!("About dialog opened"); + } +} + fn main() { // Start server BEFORE Tauri let server_child = start_server(); @@ -89,22 +311,20 @@ fn main() { std::process::exit(1); } - // Wait for server to be ready - if !wait_for_server(30) { - eprintln!("ERROR: Server failed to become ready!"); - std::process::exit(1); - } - - // Now start Tauri tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .manage(ServerProcess(Mutex::new(server_child))) .setup(|app| { - // Create window pointing to our server - let _window = WebviewWindowBuilder::new( + // Create menu + let menu = create_menu(app.handle())?; + app.set_menu(menu)?; + + // Create loading window first + let loading_url = format!("data:text/html,{}", urlencoding::encode(LOADING_HTML)); + let window = WebviewWindowBuilder::new( app, "main", - WebviewUrl::External(SERVER_URL.parse().unwrap()) + WebviewUrl::External(loading_url.parse().unwrap()) ) .title("KJV Study") .inner_size(1200.0, 800.0) @@ -112,14 +332,81 @@ fn main() { .center() .build()?; - println!("Window created, loading {}", SERVER_URL); + // Wait for server in background, then navigate + let window_clone = window.clone(); + std::thread::spawn(move || { + if wait_for_server(30) { + let _ = window_clone.eval(&format!("window.location.href = '{}'", SERVER_URL)); + println!("Navigated to server"); + } else { + let _ = window_clone.eval( + "document.body.innerHTML = '

Failed to start server

'" + ); + } + }); + Ok(()) }) + .on_menu_event(|app, event| { + let window = app.get_webview_window("main"); + + match event.id().as_ref() { + "about" => show_about_dialog(app), + "website" => { + let _ = tauri_plugin_shell::ShellExt::shell(app).open("https://kjvstudy.org", None); + } + // Navigation + "nav_home" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}'", SERVER_URL)); } + } + "nav_books" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/books'", SERVER_URL)); } + } + "nav_search" => { + if let Some(w) = window { let _ = w.eval("document.querySelector('input[type=search]')?.focus()"); } + } + "nav_votd" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/verse-of-the-day'", SERVER_URL)); } + } + "nav_random" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/random'", SERVER_URL)); } + } + // Study + "study_guides" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/study-guides'", SERVER_URL)); } + } + "topics" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/topics'", SERVER_URL)); } + } + "stories" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/stories'", SERVER_URL)); } + } + "strongs" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/strongs'", SERVER_URL)); } + } + "interlinear" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/interlinear'", SERVER_URL)); } + } + "reading_plans" => { + if let Some(w) = window { let _ = w.eval(&format!("window.location.href = '{}/reading-plans'", SERVER_URL)); } + } + // View + "reload" => { + if let Some(w) = window { let _ = w.eval("window.location.reload()"); } + } + "back" => { + if let Some(w) = window { let _ = w.eval("window.history.back()"); } + } + "forward" => { + if let Some(w) = window { let _ = w.eval("window.history.forward()"); } + } + _ => {} + } + }) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { if let RunEvent::Exit = event { - // Clean up: kill the server process let state = app_handle.state::(); if let Some(mut child) = state.0.lock().unwrap().take() { println!("Shutting down server...");