sovereignty over app lifecycle

This commit is contained in:
2025-11-25 10:15:29 +08:00
parent 96bb3ffee2
commit dbf6747c18
20 changed files with 842 additions and 215 deletions

668
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,8 +18,16 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["macos-private-api", "unstable"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
tauri-plugin-global-shortcut = "2"
tauri-plugin-positioner = "2"
reqwest = { version = "0.12.23", features = ["json", "native-tls"] }
ts-rs = "11.0.1"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2"
tauri-plugin-positioner = "2"

41
src-tauri/src/app.rs Normal file
View File

@@ -0,0 +1,41 @@
use tauri::Manager;
use tauri_plugin_positioner::WindowExt;
use crate::{
get_app_handle,
services::overlay::{overlay_fullscreen, SCENE_WINDOW_LABEL},
};
pub async fn start_fdoll() {
initialize_session().await;
}
pub async fn initialize_session() {
let webview_window = tauri::WebviewWindowBuilder::new(
get_app_handle(),
SCENE_WINDOW_LABEL,
tauri::WebviewUrl::App("/scene".into()),
)
.title("Friendolls Scene")
.inner_size(600.0, 500.0)
.resizable(false)
.decorations(false)
.transparent(true)
.shadow(false)
.visible(true)
.skip_taskbar(true)
.always_on_top(true)
.visible_on_all_workspaces(true)
.build()
.expect("Failed to display scene screen");
webview_window
.move_window(tauri_plugin_positioner::Position::Center)
.unwrap();
let window = get_app_handle().get_window(webview_window.label()).unwrap();
overlay_fullscreen(&window).unwrap();
window.set_ignore_cursor_events(true).unwrap();
println!("Scene window initialized.");
}

View File

@@ -0,0 +1,4 @@
pub mod models;
pub mod services;
pub mod state;
pub mod utilities;

View File

@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
pub struct AppConfig {
pub api_base_url: Option<String>,
}

View File

@@ -0,0 +1 @@
pub mod app_config;

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,26 @@
// in app-core/src/state.rs
use crate::{core::models::app_config::AppConfig, lock_w};
use reqwest::Client;
use std::sync::{Arc, LazyLock, RwLock};
#[derive(Default)]
pub struct AppState {
pub app_config: Option<AppConfig>,
pub http_client: Client,
}
// Global application state
// FDOLL = Multiplayer Todo App
// Read / write this state via the `lock_r!` / `lock_w!` macros from `fdoll-core::utilities`
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
pub fn init_fdoll_state() {
{
let mut guard = lock_w!(FDOLL);
guard.app_config = Some(AppConfig {
api_base_url: Some("http://sandbox:3000".to_string()),
});
guard.http_client = reqwest::Client::new();
}
}

View File

@@ -0,0 +1,29 @@
#[macro_export]
macro_rules! lock_r {
($rwlock:expr) => {{
match $rwlock.read() {
Ok(guard) => guard,
Err(_) => panic!(
"Failed to acquire read lock on {} at {}:{}",
stringify!($rwlock),
file!(),
line!()
),
}
}};
}
#[macro_export]
macro_rules! lock_w {
($rwlock:expr) => {{
match $rwlock.write() {
Ok(guard) => guard,
Err(_) => panic!(
"Failed to acquire write lock on {} at {}:{}",
stringify!($rwlock),
file!(),
line!()
),
}
}};
}

View File

@@ -1,8 +1,50 @@
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
mod app;
mod core;
mod services;
/// Tauri app handle
pub fn get_app_handle<'a>() -> &'a tauri::AppHandle<tauri::Wry> {
APP_HANDLE
.get()
.expect("get_app_handle called but app is still not initialized")
}
fn setup_fdoll() -> Result<(), tauri::Error> {
core::state::init_fdoll_state();
tokio::spawn(async move { app::start_fdoll().await });
Ok(())
}
fn register_app_events(event: tauri::RunEvent) {
match event {
tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() {
api.prevent_exit();
} else {
println!("exit code: {:?}", code);
}
}
_ => {}
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_positioner::init())
// .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.invoke_handler(tauri::generate_handler![])
.run(tauri::generate_context!())
.expect("error while running tauri application");
.setup(|app| {
APP_HANDLE
.set(app.handle().to_owned())
.expect("Failed to init app handle.");
setup_fdoll().expect("Failed to setup app.");
Ok(())
})
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|_, event| register_app_events(event));
}

View File

@@ -1,6 +1,7 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
#[tokio::main]
async fn main() {
friendolls_desktop_lib::run()
}

View File

@@ -0,0 +1 @@
pub mod overlay;

View File

@@ -0,0 +1,25 @@
use crate::get_app_handle;
pub static SCENE_WINDOW_LABEL: &str = "scene";
pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> {
// Get the primary monitor
let monitor = get_app_handle().primary_monitor()?.unwrap();
// Get the work area (usable space, excluding menu bar/dock/notch)
let work_area = monitor.work_area();
// Set window position to top-left of the work area
window.set_position(tauri::PhysicalPosition {
x: work_area.position.x,
y: work_area.position.y,
})?;
// Set window size to match work area size
window.set_size(tauri::PhysicalSize {
width: work_area.size.width,
height: work_area.size.height,
})?;
Ok(())
}

View File

@@ -10,16 +10,11 @@
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "friendolls-desktop",
"width": 800,
"height": 600
}
],
"windows": [],
"security": {
"csp": null
}
},
"macOSPrivateApi": true
},
"bundle": {
"active": true,