diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7429e33..306fb5d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["macos-private-api", "unstable"] } +tauri = { version = "2", features = ["macos-private-api", "unstable", "tray-icon"] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 949c0ed..30b27bc 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,7 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main", "scene", "preferences"], + "windows": ["main", "scene", "app_menu"], "permissions": [ "core:default", "opener:default", diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 6bfdbfa..3bff1c7 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -1,20 +1,19 @@ use tracing::info; use crate::{ - services::{ - auth::get_tokens, preferences::create_preferences_window, scene::create_scene_window, - }, + services::{auth::get_tokens, scene::open_scene_window}, state::init_app_data, + system_tray::init_system_tray, }; pub async fn start_fdoll() { + init_system_tray(); bootstrap().await; } async fn construct_app() { init_app_data().await; - create_scene_window(); - create_preferences_window(); + open_scene_window(); } pub async fn bootstrap() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 454f127..e69fc42 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ mod models; mod remotes; mod services; mod state; +mod system_tray; mod utilities; /// Tauri app handle @@ -51,6 +52,13 @@ fn get_app_data() -> Result { return Ok(guard.app_data.clone()); } +#[tauri::command] +fn quit_app() -> Result<(), String> { + let app_handle = get_app_handle(); + app_handle.exit(0); + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -59,7 +67,8 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ start_cursor_tracking, - get_app_data + get_app_data, + quit_app ]) .setup(|app| { APP_HANDLE diff --git a/src-tauri/src/services/app_menu.rs b/src-tauri/src/services/app_menu.rs new file mode 100644 index 0000000..6ccfb92 --- /dev/null +++ b/src-tauri/src/services/app_menu.rs @@ -0,0 +1,43 @@ +use tauri::Manager; +use tracing::{error, info}; + +use crate::get_app_handle; + +static APP_MENU_WINDOW_LABEL: &str = "app_menu"; + +pub fn open_app_menu_window() { + let app_handle = get_app_handle(); + let existing_webview_window = app_handle.get_window(APP_MENU_WINDOW_LABEL); + + if let Some(window) = existing_webview_window { + window.show().unwrap(); + return; + } + + match tauri::WebviewWindowBuilder::new( + app_handle, + APP_MENU_WINDOW_LABEL, + tauri::WebviewUrl::App("/app-menu".into()), + ) + .title("Friendolls") + .inner_size(600.0, 500.0) + .resizable(true) + .decorations(true) + .transparent(true) + .shadow(true) + .visible(true) + .skip_taskbar(false) + .always_on_top(false) + .visible_on_all_workspaces(false) + .build() + { + Ok(window) => { + info!("{} window builder succeeded", APP_MENU_WINDOW_LABEL); + window + } + Err(e) => { + error!("Failed to build {} window: {}", APP_MENU_WINDOW_LABEL, e); + return; + } + }; +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 3f5aedb..718d8ac 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,5 +1,5 @@ +pub mod app_menu; pub mod auth; pub mod cursor; -pub mod preferences; pub mod scene; pub mod ws; diff --git a/src-tauri/src/services/preferences.rs b/src-tauri/src/services/preferences.rs deleted file mode 100644 index 41a6655..0000000 --- a/src-tauri/src/services/preferences.rs +++ /dev/null @@ -1,35 +0,0 @@ -use tracing::{error, info}; - -use crate::get_app_handle; - -pub fn create_preferences_window() { - let webview_window = match tauri::WebviewWindowBuilder::new( - get_app_handle(), - "preferences", - tauri::WebviewUrl::App("/preferences".into()), - ) - .title("Friendolls Preferences") - .inner_size(600.0, 500.0) - .resizable(true) - .decorations(true) - .transparent(false) - .shadow(true) - .visible(true) - .skip_taskbar(false) - .always_on_top(false) - .visible_on_all_workspaces(false) - .build() - { - Ok(window) => { - info!("Preferences window builder succeeded"); - window - } - Err(e) => { - error!("Failed to build Preferences window: {}", e); - return; - } - }; - - #[cfg(debug_assertions)] - webview_window.open_devtools(); -} diff --git a/src-tauri/src/services/scene.rs b/src-tauri/src/services/scene.rs index fd5fbab..b343831 100644 --- a/src-tauri/src/services/scene.rs +++ b/src-tauri/src/services/scene.rs @@ -22,10 +22,18 @@ pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> { Ok(()) } -pub fn create_scene_window() { +pub fn open_scene_window() { + let app_handle = get_app_handle(); + let existing_webview_window = app_handle.get_window(SCENE_WINDOW_LABEL); + + if let Some(window) = existing_webview_window { + window.show().unwrap(); + return; + } + info!("Starting scene creation..."); let webview_window = match tauri::WebviewWindowBuilder::new( - get_app_handle(), + app_handle, SCENE_WINDOW_LABEL, tauri::WebviewUrl::App("/scene".into()), ) diff --git a/src-tauri/src/system_tray.rs b/src-tauri/src/system_tray.rs new file mode 100644 index 0000000..4197bf7 --- /dev/null +++ b/src-tauri/src/system_tray.rs @@ -0,0 +1,40 @@ +use tauri::{ + menu::{Menu, MenuItem}, + tray::TrayIconBuilder, +}; +use tracing::error; + +use crate::{get_app_handle, services::app_menu::open_app_menu_window}; + +pub fn init_system_tray() { + let app = get_app_handle(); + + let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap(); + let open_app_menu_i = + MenuItem::with_id(app, "open-app-menu", "Open App Menu", true, None::<&str>).unwrap(); + + let menu = match Menu::with_items(app, &[&open_app_menu_i, &quit_i]) { + Ok(it) => it, + Err(err) => todo!("Handle error: {}", err), + }; + + match TrayIconBuilder::new() + .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "quit" => { + app.exit(0); + } + "open-app-menu" => { + open_app_menu_window(); + } + _ => { + error!("menu item {:?} not handled", event.id); + } + }) + .icon(app.default_window_icon().unwrap().clone()) + .build(app) + { + Ok(it) => it, + Err(err) => todo!("Handle error: {}", err), + }; +} diff --git a/src/app.css b/src/app.css index 05a4b12..5a7fd05 100644 --- a/src/app.css +++ b/src/app.css @@ -2,75 +2,77 @@ @plugin "daisyui"; @plugin "daisyui/theme" { - name: "proper"; - default: true; - prefersdark: false; - color-scheme: "light"; - --color-base-100: oklch(96% 0.001 286.375); - --color-base-200: oklch(92% 0.004 286.32); - --color-base-300: oklch(87% 0.006 286.286); - --color-base-content: oklch(44% 0.017 285.786); - --color-primary: oklch(71% 0.143 215.221); - --color-primary-content: oklch(98% 0.019 200.873); - --color-secondary: oklch(67% 0.182 276.935); - --color-secondary-content: oklch(93% 0.034 272.788); - --color-accent: oklch(93% 0.032 255.585); - --color-accent-content: oklch(38% 0.063 188.416); - --color-neutral: oklch(96% 0.001 286.375); - --color-neutral-content: oklch(0% 0 0); - --color-info: oklch(58% 0.158 241.966); - --color-info-content: oklch(100% 0 0); - --color-success: oklch(76% 0.233 130.85); - --color-success-content: oklch(98% 0.031 120.757); - --color-warning: oklch(66% 0.179 58.318); - --color-warning-content: oklch(100% 0 0); - --color-error: oklch(70% 0.191 22.216); - --color-error-content: oklch(100% 0 0); - --radius-selector: 0.5rem; - --radius-field: 0.5rem; - --radius-box: 0.5rem; - --size-selector: 0.25rem; - --size-field: 0.25rem; - --border: 1px; - --depth: 1; - --noise: 1; + name: "proper"; + default: true; + prefersdark: false; + color-scheme: "light"; + --color-base-50: 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-300: oklch(87% 0.006 286.286); + --color-base-content: oklch(44% 0.017 285.786); + --color-primary: oklch(71% 0.143 215.221); + --color-primary-content: oklch(98% 0.019 200.873); + --color-secondary: oklch(67% 0.182 276.935); + --color-secondary-content: oklch(93% 0.034 272.788); + --color-accent: oklch(93% 0.032 255.585); + --color-accent-content: oklch(38% 0.063 188.416); + --color-neutral: oklch(96% 0.001 286.375); + --color-neutral-content: oklch(0% 0 0); + --color-info: oklch(58% 0.158 241.966); + --color-info-content: oklch(100% 0 0); + --color-success: oklch(76% 0.233 130.85); + --color-success-content: oklch(98% 0.031 120.757); + --color-warning: oklch(66% 0.179 58.318); + --color-warning-content: oklch(100% 0 0); + --color-error: oklch(70% 0.191 22.216); + --color-error-content: oklch(100% 0 0); + --radius-selector: 0.25rem; + --radius-field: 0.25rem; + --radius-box: 0.25rem; + --size-selector: 0.1875rem; + --size-field: 0.1875rem; + --border: 1px; + --depth: 1; + --noise: 1; } @plugin "daisyui/theme" { - name: "properdark"; - default: false; - prefersdark: true; - color-scheme: "dark"; - --color-base-100: oklch(26% 0 0); - --color-base-200: oklch(37% 0 0); - --color-base-300: oklch(43% 0 0); - --color-base-content: oklch(100% 0 0); - --color-primary: oklch(86% 0.127 207.078); - --color-primary-content: oklch(39% 0.07 227.392); - --color-secondary: oklch(87% 0.065 274.039); - --color-secondary-content: oklch(35% 0.144 278.697); - --color-accent: oklch(36.2% 0.076 265.6); - --color-accent-content: oklch(97% 0.014 254.604); - --color-neutral: oklch(37% 0.01 67.558); - --color-neutral-content: oklch(100% 0 0); - --color-info: oklch(64.4% 0.12 237.3); - --color-info-content: oklch(100% 0 0); - --color-success: oklch(76% 0.233 130.85); - --color-success-content: oklch(0% 0 0); - --color-warning: oklch(87% 0.169 91.605); - --color-warning-content: oklch(0% 0 0); - --color-error: oklch(63% 0.237 25.331); - --color-error-content: oklch(100% 0 0); - --radius-selector: 0.5rem; - --radius-field: 0.5rem; - --radius-box: 0.5rem; - --size-selector: 0.25rem; - --size-field: 0.25rem; - --border: 1px; - --depth: 1; - --noise: 1; + name: "properdark"; + default: false; + prefersdark: true; + color-scheme: "dark"; + --color-base-50: oklch(14% 0 0); + --color-base-100: oklch(26% 0 0); + --color-base-200: oklch(37% 0 0); + --color-base-300: oklch(43% 0 0); + --color-base-content: oklch(100% 0 0); + --color-primary: oklch(86% 0.127 207.078); + --color-primary-content: oklch(39% 0.07 227.392); + --color-secondary: oklch(87% 0.065 274.039); + --color-secondary-content: oklch(35% 0.144 278.697); + --color-accent: oklch(36.2% 0.076 265.6); + --color-accent-content: oklch(97% 0.014 254.604); + --color-neutral: oklch(37% 0.01 67.558); + --color-neutral-content: oklch(100% 0 0); + --color-info: oklch(64.4% 0.12 237.3); + --color-info-content: oklch(100% 0 0); + --color-success: oklch(76% 0.233 130.85); + --color-success-content: oklch(0% 0 0); + --color-warning: oklch(87% 0.169 91.605); + --color-warning-content: oklch(0% 0 0); + --color-error: oklch(63% 0.237 25.331); + --color-error-content: oklch(100% 0 0); + --radius-selector: 0.25rem; + --radius-field: 0.25rem; + --radius-box: 0.25rem; + --size-selector: 0.1875rem; + --size-field: 0.1875rem; + --border: 1px; + --depth: 1; + --noise: 1; } :root { - background-color: transparent; + background-color: transparent; } diff --git a/src/events/app-data.ts b/src/events/app-data.ts new file mode 100644 index 0000000..c0aa7f8 --- /dev/null +++ b/src/events/app-data.ts @@ -0,0 +1,40 @@ +import { writable } from "svelte/store"; +import { type AppData } from "../types/bindings/AppData"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { invoke } from "@tauri-apps/api/core"; + +export let appData = writable(null); + +let unlisten: UnlistenFn | null = null; +let isListening = false; + +export async function initAppDataListener() { + try { + if (isListening) return; + appData.set(await invoke("get_app_data")); + unlisten = await listen("app-data-refreshed", (event) => { + console.log("app-data-refreshed", event.payload); + appData.set(event.payload); + }); + + isListening = true; + } catch (error) { + console.error(error); + throw error; + } +} + +export function stopAppDataListener() { + if (unlisten) { + unlisten(); + unlisten = null; + isListening = false; + } +} + +// Handle HMR (Hot Module Replacement) cleanup +if (import.meta.hot) { + import.meta.hot.dispose(() => { + stopAppDataListener(); + }); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 44918e1..b329caf 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,14 +2,16 @@ import { browser } from "$app/environment"; import { onMount, onDestroy } from "svelte"; import { initCursorTracking, stopCursorTracking } from "../events/cursor"; + import { initAppDataListener } from "../events/app-data"; let { children } = $props(); if (browser) { onMount(async () => { try { await initCursorTracking(); + await initAppDataListener(); } catch (err) { - console.error("[Scene] Failed to initialize cursor tracking:", err); + console.error("[Scene] Failed to initialize event listeners:", err); } }); diff --git a/src/routes/app-menu/+page.svelte b/src/routes/app-menu/+page.svelte new file mode 100644 index 0000000..7e993fb --- /dev/null +++ b/src/routes/app-menu/+page.svelte @@ -0,0 +1,46 @@ + + +
+
+
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+
+
+
+
diff --git a/src/routes/app-menu/tabs/friends.svelte b/src/routes/app-menu/tabs/friends.svelte new file mode 100644 index 0000000..b863325 --- /dev/null +++ b/src/routes/app-menu/tabs/friends.svelte @@ -0,0 +1,7 @@ + + +
+

{$appData?.user?.name}'s friends

+
diff --git a/src/routes/app-menu/tabs/preferences.svelte b/src/routes/app-menu/tabs/preferences.svelte new file mode 100644 index 0000000..5f03ca8 --- /dev/null +++ b/src/routes/app-menu/tabs/preferences.svelte @@ -0,0 +1,21 @@ + + +
+
+

{$appData?.user?.name}'s preferences

+
+
+
+
+ +
+
+
diff --git a/src/routes/app-menu/tabs/your-dolls.svelte b/src/routes/app-menu/tabs/your-dolls.svelte new file mode 100644 index 0000000..dae3e82 --- /dev/null +++ b/src/routes/app-menu/tabs/your-dolls.svelte @@ -0,0 +1,7 @@ + + +
+

{$appData?.user?.name}

+
diff --git a/src/routes/preferences/+page.svelte b/src/routes/preferences/+page.svelte deleted file mode 100644 index 32c45ee..0000000 --- a/src/routes/preferences/+page.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -
-
-
-

Preferences

-

Settings and configuration

-
- -
-

Cursor Position (Multi-Window Test)

-
- - Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw - .y}) - - - Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen - .mapped.y}) - -
-
- -
-