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 = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["macos-private-api", "unstable"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" 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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_positioner::init())
// .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.invoke_handler(tauri::generate_handler![]) .invoke_handler(tauri::generate_handler![])
.run(tauri::generate_context!()) .setup(|app| {
.expect("error while running tauri application"); 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!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { #[tokio::main]
async fn main() {
friendolls_desktop_lib::run() 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" "frontendDist": "../build"
}, },
"app": { "app": {
"windows": [ "windows": [],
{
"title": "friendolls-desktop",
"width": 800,
"height": 600
}
],
"security": { "security": {
"csp": null "csp": null
} },
"macOSPrivateApi": true
}, },
"bundle": { "bundle": {
"active": true, "active": true,

View File

@@ -1,75 +1,76 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "daisyui"; @plugin "daisyui";
@plugin "daisyui/theme" { @plugin "daisyui/theme" {
name: "proper"; name: "proper";
default: true; default: true;
prefersdark: false; prefersdark: false;
color-scheme: "light"; color-scheme: "light";
--color-base-100: oklch(96% 0.001 286.375); --color-base-100: oklch(96% 0.001 286.375);
--color-base-200: oklch(92% 0.004 286.32); --color-base-200: oklch(92% 0.004 286.32);
--color-base-300: oklch(87% 0.006 286.286); --color-base-300: oklch(87% 0.006 286.286);
--color-base-content: oklch(44% 0.017 285.786); --color-base-content: oklch(44% 0.017 285.786);
--color-primary: oklch(71% 0.143 215.221); --color-primary: oklch(71% 0.143 215.221);
--color-primary-content: oklch(98% 0.019 200.873); --color-primary-content: oklch(98% 0.019 200.873);
--color-secondary: oklch(67% 0.182 276.935); --color-secondary: oklch(67% 0.182 276.935);
--color-secondary-content: oklch(93% 0.034 272.788); --color-secondary-content: oklch(93% 0.034 272.788);
--color-accent: oklch(93% 0.032 255.585); --color-accent: oklch(93% 0.032 255.585);
--color-accent-content: oklch(38% 0.063 188.416); --color-accent-content: oklch(38% 0.063 188.416);
--color-neutral: oklch(96% 0.001 286.375); --color-neutral: oklch(96% 0.001 286.375);
--color-neutral-content: oklch(0% 0 0); --color-neutral-content: oklch(0% 0 0);
--color-info: oklch(58% 0.158 241.966); --color-info: oklch(58% 0.158 241.966);
--color-info-content: oklch(100% 0 0); --color-info-content: oklch(100% 0 0);
--color-success: oklch(76% 0.233 130.85); --color-success: oklch(76% 0.233 130.85);
--color-success-content: oklch(98% 0.031 120.757); --color-success-content: oklch(98% 0.031 120.757);
--color-warning: oklch(66% 0.179 58.318); --color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(100% 0 0); --color-warning-content: oklch(100% 0 0);
--color-error: oklch(70% 0.191 22.216); --color-error: oklch(70% 0.191 22.216);
--color-error-content: oklch(100% 0 0); --color-error-content: oklch(100% 0 0);
--radius-selector: 0.5rem; --radius-selector: 0.5rem;
--radius-field: 0.5rem; --radius-field: 0.5rem;
--radius-box: 0.5rem; --radius-box: 0.5rem;
--size-selector: 0.25rem; --size-selector: 0.25rem;
--size-field: 0.25rem; --size-field: 0.25rem;
--border: 1px; --border: 1px;
--depth: 1; --depth: 1;
--noise: 1; --noise: 1;
} }
@plugin "daisyui/theme" { @plugin "daisyui/theme" {
name: "properdark"; name: "properdark";
default: false; default: false;
prefersdark: true; prefersdark: true;
color-scheme: "dark"; color-scheme: "dark";
--color-base-100: oklch(26% 0 0); --color-base-100: oklch(26% 0 0);
--color-base-200: oklch(37% 0 0); --color-base-200: oklch(37% 0 0);
--color-base-300: oklch(43% 0 0); --color-base-300: oklch(43% 0 0);
--color-base-content: oklch(100% 0 0); --color-base-content: oklch(100% 0 0);
--color-primary: oklch(86% 0.127 207.078); --color-primary: oklch(86% 0.127 207.078);
--color-primary-content: oklch(39% 0.07 227.392); --color-primary-content: oklch(39% 0.07 227.392);
--color-secondary: oklch(87% 0.065 274.039); --color-secondary: oklch(87% 0.065 274.039);
--color-secondary-content: oklch(35% 0.144 278.697); --color-secondary-content: oklch(35% 0.144 278.697);
--color-accent: oklch(36.2% 0.076 265.6); --color-accent: oklch(36.2% 0.076 265.6);
--color-accent-content: oklch(97% 0.014 254.604); --color-accent-content: oklch(97% 0.014 254.604);
--color-neutral: oklch(37% 0.01 67.558); --color-neutral: oklch(37% 0.01 67.558);
--color-neutral-content: oklch(100% 0 0); --color-neutral-content: oklch(100% 0 0);
--color-info: oklch(64.4% 0.12 237.3); --color-info: oklch(64.4% 0.12 237.3);
--color-info-content: oklch(100% 0 0); --color-info-content: oklch(100% 0 0);
--color-success: oklch(76% 0.233 130.85); --color-success: oklch(76% 0.233 130.85);
--color-success-content: oklch(0% 0 0); --color-success-content: oklch(0% 0 0);
--color-warning: oklch(87% 0.169 91.605); --color-warning: oklch(87% 0.169 91.605);
--color-warning-content: oklch(0% 0 0); --color-warning-content: oklch(0% 0 0);
--color-error: oklch(63% 0.237 25.331); --color-error: oklch(63% 0.237 25.331);
--color-error-content: oklch(100% 0 0); --color-error-content: oklch(100% 0 0);
--radius-selector: 0.5rem; --radius-selector: 0.5rem;
--radius-field: 0.5rem; --radius-field: 0.5rem;
--radius-box: 0.5rem; --radius-box: 0.5rem;
--size-selector: 0.25rem; --size-selector: 0.25rem;
--size-field: 0.25rem; --size-field: 0.25rem;
--border: 1px; --border: 1px;
--depth: 1; --depth: 1;
--noise: 1; --noise: 1;
}
:root {
background-color: transparent;
} }

View File

@@ -1,13 +1,13 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Friendolls</title> <title>Friendolls</title>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,7 @@
<script>
let { children } = $props();
</script>
<div class="size-full bg-transparent">
{@render children?.()}
</div>

View File

@@ -3,3 +3,4 @@
// See: https://svelte.dev/docs/kit/single-page-apps // See: https://svelte.dev/docs/kit/single-page-apps
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const ssr = false; export const ssr = false;
import "../app.css";

View File

@@ -1,7 +1,3 @@
<script lang="ts">
import "../app.css";
</script>
<main class="card-body"> <main class="card-body">
<button class="btn btn-primary">Hello TailwindCSS!</button> <button class="btn btn-primary">Hello TailwindCSS!</button>
</main> </main>

View File

@@ -0,0 +1,10 @@
<div class="w-svw h-svh p-4">
<div
class="size-max mx-auto bg-base-100 border-base-200 border px-4 py-3 rounded-xl"
>
<div class="flex flex-col text-center">
<p class="text-xl">Friendolls</p>
<p class="text-sm opacity-50">Scene Screen</p>
</div>
</div>
</div>