From ca995899fed232beca34e91d6da03d4bff888635 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Mon, 5 Jan 2026 16:40:27 +0800 Subject: [PATCH] hotkey to activate neko interactions --- src-tauri/src/services/scene.rs | 124 ++++++++++++++++++++++++++++- src/events/scene-interactive.ts | 37 +++++++++ src/routes/+layout.svelte | 6 ++ src/routes/scene/+page.svelte | 44 +++++----- src/routes/scene/DesktopPet.svelte | 16 ++-- 5 files changed, 196 insertions(+), 31 deletions(-) create mode 100644 src/events/scene-interactive.ts diff --git a/src-tauri/src/services/scene.rs b/src-tauri/src/services/scene.rs index 7b378db..394fe0c 100644 --- a/src-tauri/src/services/scene.rs +++ b/src-tauri/src/services/scene.rs @@ -1,10 +1,125 @@ -use crate::get_app_handle; -use tauri::Manager; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; + +use device_query::{DeviceQuery, DeviceState, Keycode}; +use once_cell::sync::OnceCell; +use tauri::{Emitter, Manager}; use tauri_plugin_positioner::WindowExt; -use tracing::{error, info}; +use tracing::{error, info, warn}; + +use crate::get_app_handle; + pub static SCENE_WINDOW_LABEL: &str = "scene"; pub static SPLASH_WINDOW_LABEL: &str = "splash"; +static SCENE_INTERACTIVE_STATE: OnceCell> = OnceCell::new(); +static MODIFIER_LISTENER_INIT: OnceCell<()> = OnceCell::new(); + +fn scene_interactive_state() -> Arc { + SCENE_INTERACTIVE_STATE + .get_or_init(|| Arc::new(AtomicBool::new(false))) + .clone() +} + +fn update_scene_interactive(interactive: bool) { + let app_handle = get_app_handle(); + + if let Some(window) = app_handle.get_window(SCENE_WINDOW_LABEL) { + if let Err(e) = window.set_ignore_cursor_events(!interactive) { + error!("Failed to toggle scene cursor events: {}", e); + } + + if let Err(e) = window.emit("scene-interactive", &interactive) { + error!("Failed to emit scene interactive event: {}", e); + } + } else { + warn!("Scene window not available for interactive update"); + } +} + +#[cfg(target_os = "macos")] +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn AXIsProcessTrusted() -> bool; +} + +fn start_scene_modifier_listener() { + MODIFIER_LISTENER_INIT.get_or_init(|| { + let state = scene_interactive_state(); + update_scene_interactive(false); + + let app_handle = get_app_handle().clone(); + + #[cfg(target_os = "macos")] + unsafe { + info!("Accessibility status: {}", AXIsProcessTrusted()); + if !AXIsProcessTrusted() { + // Warning only - polling might work without explicit permissions for just key state in some contexts, + // or we just want to avoid the crash. We'll show the dialog but not return early if we want to try anyway. + // However, usually global key monitoring requires it. + // Let's show the dialog but NOT return, to try polling. + // Or better, let's keep the return if we think it won't work at all, + // but since the crash was the main issue, let's try to proceed safely. + // For now, I will keep the dialog and the return to encourage users to enable it, + // as it's likely needed for global input monitoring. + + // On second thought, let's keep the return to be safe and clear to the user. + error!("Accessibility permissions not granted. Global modifier listener will NOT start."); + + use tauri_plugin_dialog::DialogExt; + use tauri_plugin_dialog::MessageDialogBuilder; + use tauri_plugin_dialog::MessageDialogKind; + + MessageDialogBuilder::new( + app_handle.dialog().clone(), + "Missing Permissions", + "Friendolls needs Accessibility permissions to detect the Alt key for interactivity. Please grant permissions in System Settings -> Privacy & Security -> Accessibility and restart the app.", + ) + .kind(MessageDialogKind::Warning) + .show(|_| {}); + + return; + } + } + + // Spawn a thread for polling key state + thread::spawn(move || { + let device_state = DeviceState::new(); + let mut last_interactive = false; + + loop { + let keys = device_state.get_keys(); + // Check for Alt key (Option on Mac) + let interactive = (keys.contains(&Keycode::LAlt) || keys.contains(&Keycode::RAlt)) || keys.contains(&Keycode::Command); + + if interactive != last_interactive { + // State changed + info!("Key down state chanegd!"); + let previous = state.swap(interactive, Ordering::SeqCst); + if previous != interactive { + if let Some(window) = app_handle.get_window(SCENE_WINDOW_LABEL) { + if let Err(err) = window.set_ignore_cursor_events(!interactive) { + error!("Failed to toggle scene cursor events: {}", err); + } + + if let Err(err) = window.emit("scene-interactive", &interactive) { + error!("Failed to emit scene interactive event: {}", err); + } + } else { + warn!("Scene window not available for interactive update"); + } + } + last_interactive = interactive; + } + + // Poll every 100ms + thread::sleep(std::time::Duration::from_millis(100)); + } + }); + }); +} + pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> { // Get the primary monitor let monitor = get_app_handle().primary_monitor()?.unwrap(); @@ -142,6 +257,9 @@ pub fn open_scene_window() { return; } + // Start global modifier listener once scene window exists + start_scene_modifier_listener(); + #[cfg(debug_assertions)] webview_window.open_devtools(); diff --git a/src/events/scene-interactive.ts b/src/events/scene-interactive.ts new file mode 100644 index 0000000..0d75be3 --- /dev/null +++ b/src/events/scene-interactive.ts @@ -0,0 +1,37 @@ +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { writable } from "svelte/store"; + +export const sceneInteractive = writable(false); + +let unlisten: UnlistenFn | null = null; +let isListening = false; + +export async function initSceneInteractiveListener() { + if (isListening) return; + + try { + // ensure initial default matches backend default + sceneInteractive.set(false); + unlisten = await listen("scene-interactive", (event) => { + sceneInteractive.set(Boolean(event.payload)); + }); + isListening = true; + } catch (error) { + console.error("Failed to initialize scene interactive listener:", error); + throw error; + } +} + +export function stopSceneInteractiveListener() { + if (unlisten) { + unlisten(); + unlisten = null; + isListening = false; + } +} + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + stopSceneInteractiveListener(); + }); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index be9c067..ee18b0a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,6 +3,10 @@ import { onMount, onDestroy } from "svelte"; import { initCursorTracking, stopCursorTracking } from "../events/cursor"; import { initAppDataListener } from "../events/app-data"; + import { + initSceneInteractiveListener, + stopSceneInteractiveListener, + } from "../events/scene-interactive"; let { children } = $props(); if (browser) { @@ -10,6 +14,7 @@ try { await initCursorTracking(); await initAppDataListener(); + await initSceneInteractiveListener(); } catch (err) { console.error("Failed to initialize event listeners:", err); } @@ -17,6 +22,7 @@ onDestroy(() => { stopCursorTracking(); + stopSceneInteractiveListener(); }); } diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 73a2109..62df956 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -5,12 +5,15 @@ friendsActiveDolls, } from "../../events/cursor"; import { appData } from "../../events/app-data"; + import { sceneInteractive } from "../../events/scene-interactive"; import DesktopPet from "./DesktopPet.svelte"; let innerWidth = 0; let innerHeight = 0; + $: isInteractive = $sceneInteractive; + function getFriendName(userId: string) { const friend = $appData?.friends?.find((f) => f.friend.id === userId); return friend ? friend.friend.name : userId.slice(0, 8) + "..."; @@ -30,39 +33,34 @@
-
-

Friendolls

-

Scene Screen

-
- - Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw - .y}) - - - Mapped: ({$cursorPositionOnScreen.mapped.x.toFixed(3)}, {$cursorPositionOnScreen.mapped.y.toFixed( - 3, - )}) +
+
+ + + Intercepting cursor events
+ + ({$cursorPositionOnScreen.mapped.x.toFixed(3)}, {$cursorPositionOnScreen.mapped.y.toFixed( + 3, + )}) + + {#if Object.keys($friendsCursorPositions).length > 0} -
-

Friends Online

+
{#each Object.entries($friendsCursorPositions) as [userId, position]} {@const dollConfig = getFriendDollConfig(userId)} -
+
{getFriendName(userId)}
- Raw: ({position.raw.x}, {position.raw.y}) - - - Mapped: ({position.mapped.x.toFixed(3)}, {position.mapped.y.toFixed( + ({position.mapped.x.toFixed(3)}, {position.mapped.y.toFixed( 3, )}) @@ -74,6 +72,7 @@ {/if}
+
{#if Object.keys($friendsCursorPositions).length > 0} {#each Object.entries($friendsCursorPositions) as [userId, position]} @@ -85,6 +84,7 @@ name={getFriendName(userId)} bodyColor={config?.colorScheme?.body} outlineColor={config?.colorScheme?.outline} + {isInteractive} /> {/if} {/each} diff --git a/src/routes/scene/DesktopPet.svelte b/src/routes/scene/DesktopPet.svelte index 1fd9701..b58d947 100644 --- a/src/routes/scene/DesktopPet.svelte +++ b/src/routes/scene/DesktopPet.svelte @@ -8,6 +8,7 @@ export let name = ""; export let bodyColor: string | undefined = undefined; export let outlineColor: string | undefined = undefined; + export let isInteractive = false; let nekoPosX = 32; let nekoPosY = 32; @@ -251,15 +252,18 @@ }); -
{ + console.log("clicked on neko"); + }} + class="desktop-pet flex flex-col items-center relative" style=" transform: translate({nekoPosX - 16}px, {nekoPosY - 16}px); z-index: 50; " >
{name} -
+