app menu window
This commit is contained in:
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = ["macos-private-api", "unstable"] }
|
tauri = { version = "2", features = ["macos-private-api", "unstable", "tray-icon"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main", "scene", "preferences"],
|
"windows": ["main", "scene", "app_menu"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
services::{
|
services::{auth::get_tokens, scene::open_scene_window},
|
||||||
auth::get_tokens, preferences::create_preferences_window, scene::create_scene_window,
|
|
||||||
},
|
|
||||||
state::init_app_data,
|
state::init_app_data,
|
||||||
|
system_tray::init_system_tray,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn start_fdoll() {
|
pub async fn start_fdoll() {
|
||||||
|
init_system_tray();
|
||||||
bootstrap().await;
|
bootstrap().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn construct_app() {
|
async fn construct_app() {
|
||||||
init_app_data().await;
|
init_app_data().await;
|
||||||
create_scene_window();
|
open_scene_window();
|
||||||
create_preferences_window();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn bootstrap() {
|
pub async fn bootstrap() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod models;
|
|||||||
mod remotes;
|
mod remotes;
|
||||||
mod services;
|
mod services;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod system_tray;
|
||||||
mod utilities;
|
mod utilities;
|
||||||
|
|
||||||
/// Tauri app handle
|
/// Tauri app handle
|
||||||
@@ -51,6 +52,13 @@ fn get_app_data() -> Result<AppData, String> {
|
|||||||
return Ok(guard.app_data.clone());
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -59,7 +67,8 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
start_cursor_tracking,
|
start_cursor_tracking,
|
||||||
get_app_data
|
get_app_data,
|
||||||
|
quit_app
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
APP_HANDLE
|
APP_HANDLE
|
||||||
|
|||||||
43
src-tauri/src/services/app_menu.rs
Normal file
43
src-tauri/src/services/app_menu.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
pub mod app_menu;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
pub mod preferences;
|
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -22,10 +22,18 @@ pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> {
|
|||||||
Ok(())
|
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...");
|
info!("Starting scene creation...");
|
||||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
let webview_window = match tauri::WebviewWindowBuilder::new(
|
||||||
get_app_handle(),
|
app_handle,
|
||||||
SCENE_WINDOW_LABEL,
|
SCENE_WINDOW_LABEL,
|
||||||
tauri::WebviewUrl::App("/scene".into()),
|
tauri::WebviewUrl::App("/scene".into()),
|
||||||
)
|
)
|
||||||
|
|||||||
40
src-tauri/src/system_tray.rs
Normal file
40
src-tauri/src/system_tray.rs
Normal file
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/app.css
22
src/app.css
@@ -6,6 +6,7 @@
|
|||||||
default: true;
|
default: true;
|
||||||
prefersdark: false;
|
prefersdark: false;
|
||||||
color-scheme: "light";
|
color-scheme: "light";
|
||||||
|
--color-base-50: oklch(96% 0.001 286.375);
|
||||||
--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);
|
||||||
@@ -26,11 +27,11 @@
|
|||||||
--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.25rem;
|
||||||
--radius-field: 0.5rem;
|
--radius-field: 0.25rem;
|
||||||
--radius-box: 0.5rem;
|
--radius-box: 0.25rem;
|
||||||
--size-selector: 0.25rem;
|
--size-selector: 0.1875rem;
|
||||||
--size-field: 0.25rem;
|
--size-field: 0.1875rem;
|
||||||
--border: 1px;
|
--border: 1px;
|
||||||
--depth: 1;
|
--depth: 1;
|
||||||
--noise: 1;
|
--noise: 1;
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
default: false;
|
default: false;
|
||||||
prefersdark: true;
|
prefersdark: true;
|
||||||
color-scheme: "dark";
|
color-scheme: "dark";
|
||||||
|
--color-base-50: oklch(14% 0 0);
|
||||||
--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);
|
||||||
@@ -61,11 +63,11 @@
|
|||||||
--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.25rem;
|
||||||
--radius-field: 0.5rem;
|
--radius-field: 0.25rem;
|
||||||
--radius-box: 0.5rem;
|
--radius-box: 0.25rem;
|
||||||
--size-selector: 0.25rem;
|
--size-selector: 0.1875rem;
|
||||||
--size-field: 0.25rem;
|
--size-field: 0.1875rem;
|
||||||
--border: 1px;
|
--border: 1px;
|
||||||
--depth: 1;
|
--depth: 1;
|
||||||
--noise: 1;
|
--noise: 1;
|
||||||
|
|||||||
40
src/events/app-data.ts
Normal file
40
src/events/app-data.ts
Normal file
@@ -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<AppData | null>(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<AppData>("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();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,14 +2,16 @@
|
|||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { initCursorTracking, stopCursorTracking } from "../events/cursor";
|
import { initCursorTracking, stopCursorTracking } from "../events/cursor";
|
||||||
|
import { initAppDataListener } from "../events/app-data";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
if (browser) {
|
if (browser) {
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await initCursorTracking();
|
await initCursorTracking();
|
||||||
|
await initAppDataListener();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Scene] Failed to initialize cursor tracking:", err);
|
console.error("[Scene] Failed to initialize event listeners:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
46
src/routes/app-menu/+page.svelte
Normal file
46
src/routes/app-menu/+page.svelte
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Friends from "./tabs/friends.svelte";
|
||||||
|
import Preferences from "./tabs/preferences.svelte";
|
||||||
|
import YourDolls from "./tabs/your-dolls.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-2 h-full absolute inset-0 bg-base-100 border-base-200/50 border border-t-0 rounded-b-xl"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2 h-full">
|
||||||
|
<div class="size-full">
|
||||||
|
<div class="tabs tabs-lift h-full">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="app_menu_tabs"
|
||||||
|
class="tab"
|
||||||
|
aria-label="Your Dolls"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<div class="tab-content bg-base-100 border-base-300 p-4">
|
||||||
|
<YourDolls />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="app_menu_tabs"
|
||||||
|
class="tab"
|
||||||
|
aria-label="Friends"
|
||||||
|
/>
|
||||||
|
<div class="tab-content bg-base-100 border-base-300 p-4">
|
||||||
|
<Friends />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="app_menu_tabs"
|
||||||
|
class="tab"
|
||||||
|
aria-label="Preferences"
|
||||||
|
/>
|
||||||
|
<div class="tab-content bg-base-100 border-base-300 p-4">
|
||||||
|
<Preferences />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
7
src/routes/app-menu/tabs/friends.svelte
Normal file
7
src/routes/app-menu/tabs/friends.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import { appData } from "../../../events/app-data";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{$appData?.user?.name}'s friends</p>
|
||||||
|
</div>
|
||||||
21
src/routes/app-menu/tabs/preferences.svelte
Normal file
21
src/routes/app-menu/tabs/preferences.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { appData } from "../../../events/app-data";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="size-full flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<p>{$appData?.user?.name}'s preferences</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-row justify-between">
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-error btn-sm btn-soft"
|
||||||
|
onclick={async () => {
|
||||||
|
await invoke("quit_app");
|
||||||
|
}}>Quit Friendolls</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
7
src/routes/app-menu/tabs/your-dolls.svelte
Normal file
7
src/routes/app-menu/tabs/your-dolls.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import { appData } from "../../../events/app-data";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{$appData?.user?.name}</p>
|
||||||
|
</div>
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { cursorPositionOnScreen } from "../../events/cursor";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="p-4">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div>
|
|
||||||
<p class="text-xl font-semibold">Preferences</p>
|
|
||||||
<p class="text-sm opacity-50">Settings and configuration</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-200 p-4">
|
|
||||||
<h3 class="font-semibold mb-2">Cursor Position (Multi-Window Test)</h3>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<span class="font-mono text-sm">
|
|
||||||
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw
|
|
||||||
.y})
|
|
||||||
</span>
|
|
||||||
<span class="font-mono text-sm">
|
|
||||||
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen
|
|
||||||
.mapped.y})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
onclick={() => {
|
|
||||||
invoke("get_app_data").then((data) => {
|
|
||||||
console.log("data", data);
|
|
||||||
});
|
|
||||||
}}>Fetch app data</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
Reference in New Issue
Block a user