From 75ab799a7f8287869fc9fa5743747427dcfc5af2 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Tue, 24 Mar 2026 22:52:49 +0800 Subject: [PATCH] scene configuration, neko opacity & scale --- src-tauri/src/commands/app_state.rs | 27 +++++- src-tauri/src/lib.rs | 32 ++++--- src-tauri/src/models/app_state.rs | 39 +++++++++ src-tauri/src/models/mod.rs | 1 + src-tauri/src/services/app_events.rs | 5 ++ src-tauri/src/services/app_state.rs | 42 +++++++++ src-tauri/src/services/mod.rs | 1 + src/assets/icons/image.svelte | 17 ++++ src/events/app-state.ts | 32 +++++++ src/lib/bindings.ts | 18 ++++ src/routes/+layout.svelte | 3 + src/routes/app-menu/+page.svelte | 14 ++- .../app-menu/components/doll-preview.svelte | 7 +- .../tabs/scene/neko-reposition.svelte | 74 ++++++++++++++++ .../app-menu/tabs/scene/neko-view.svelte | 85 +++++++++++++++++++ src/routes/app-menu/tabs/scene/scene.svelte | 10 +++ src/routes/client-config/+page.svelte | 6 +- src/routes/scene/+page.svelte | 17 ++-- src/routes/scene/components/neko/neko.svelte | 31 +++++-- 19 files changed, 427 insertions(+), 34 deletions(-) create mode 100644 src-tauri/src/models/app_state.rs create mode 100644 src-tauri/src/services/app_state.rs create mode 100644 src/assets/icons/image.svelte create mode 100644 src/events/app-state.ts create mode 100644 src/routes/app-menu/tabs/scene/neko-reposition.svelte create mode 100644 src/routes/app-menu/tabs/scene/neko-view.svelte create mode 100644 src/routes/app-menu/tabs/scene/scene.svelte diff --git a/src-tauri/src/commands/app_state.rs b/src-tauri/src/commands/app_state.rs index 94ce7e9..02b20de 100644 --- a/src-tauri/src/commands/app_state.rs +++ b/src-tauri/src/commands/app_state.rs @@ -1,8 +1,9 @@ use crate::{ lock_r, - models::app_data::UserData, + models::{app_data::UserData, app_state::{AppState, NekoPosition}}, services::{ app_data::{init_app_data_scoped, AppDataRefreshScope}, + app_state, friends, presence_modules::models::ModuleMetadata, sprite, @@ -45,3 +46,27 @@ pub fn get_friend_active_doll_sprites_base64() -> Result Result { + Ok(app_state::get_snapshot()) +} + +#[tauri::command] +#[specta::specta] +pub fn set_scene_setup_nekos_position(nekos_position: Option) { + app_state::set_scene_setup_nekos_position(nekos_position); +} + +#[tauri::command] +#[specta::specta] +pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) { + app_state::set_scene_setup_nekos_opacity(nekos_opacity); +} + +#[tauri::command] +#[specta::specta] +pub fn set_scene_setup_nekos_scale(nekos_scale: f32) { + app_state::set_scene_setup_nekos_scale(nekos_scale); +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eca590c..18518e8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,14 +1,12 @@ -use crate::{ - commands::app_state::get_modules, - services::{ - doll_editor::open_doll_editor_window, - scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive}, - }, +use crate::services::{ + doll_editor::open_doll_editor_window, + scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive}, }; use commands::app::{quit_app, restart_app, retry_connection}; use commands::app_state::{ - get_active_doll_sprite_base64, get_app_data, get_friend_active_doll_sprites_base64, - refresh_app_data, + get_active_doll_sprite_base64, get_app_data, get_app_state, + get_friend_active_doll_sprites_base64, get_modules, refresh_app_data, + set_scene_setup_nekos_opacity, set_scene_setup_nekos_position, set_scene_setup_nekos_scale, }; use commands::auth::{logout_and_restart, start_discord_auth, start_google_auth}; use commands::config::{get_client_config, open_client_config, save_client_config}; @@ -27,11 +25,12 @@ use tauri::async_runtime; use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode}; use crate::services::app_events::{ - ActiveDollSpriteChanged, AppDataRefreshed, AuthFlowUpdated, CreateDoll, CursorMoved, EditDoll, - FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendCursorPositionsUpdated, - FriendDisconnected, FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, - FriendUserStatusChanged, InteractionDeliveryFailed, InteractionReceived, - SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged, + ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll, + CursorMoved, EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, + FriendCursorPositionsUpdated, FriendDisconnected, FriendRequestAccepted, + FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, + InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, + SetInteractionOverlay, Unfriended, UserStatusChanged, }; static APP_HANDLE: std::sync::OnceLock> = std::sync::OnceLock::new(); @@ -68,6 +67,7 @@ pub fn run() { .error_handling(ErrorHandlingMode::Throw) .commands(collect_commands![ get_app_data, + get_app_state, get_active_doll_sprite_base64, get_friend_active_doll_sprites_base64, refresh_app_data, @@ -102,12 +102,16 @@ pub fn run() { start_discord_auth, logout_and_restart, send_interaction_cmd, - get_modules + get_modules, + set_scene_setup_nekos_position, + set_scene_setup_nekos_opacity, + set_scene_setup_nekos_scale ]) .events(collect_events![ CursorMoved, SceneInteractiveChanged, AppDataRefreshed, + AppStateChanged, ActiveDollSpriteChanged, SetInteractionOverlay, EditDoll, diff --git a/src-tauri/src/models/app_state.rs b/src-tauri/src/models/app_state.rs new file mode 100644 index 0000000..1357b60 --- /dev/null +++ b/src-tauri/src/models/app_state.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "kebab-case")] +pub enum NekoPosition { + TopLeft, + Top, + TopRight, + Left, + Right, + BottomLeft, + Bottom, + BottomRight, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct SceneSetup { + pub nekos_position: Option, + pub nekos_opacity: f32, + pub nekos_scale: f32, +} + +impl Default for SceneSetup { + fn default() -> Self { + Self { + nekos_position: None, + nekos_opacity: 1.0, + nekos_scale: 1.0, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type, Default)] +#[serde(rename_all = "camelCase")] +pub struct AppState { + pub scene_setup: SceneSetup, +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index a40165e..e8a7205 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod app_data; +pub mod app_state; pub mod dolls; pub mod event_payloads; pub mod friends; diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index eb18bcd..1f71388 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -5,6 +5,7 @@ use tauri_specta::Event; use crate::{ models::{ app_data::UserData, + app_state::AppState, event_payloads::{ FriendActiveDollChangedPayload, FriendDisconnectedPayload, FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload, @@ -47,6 +48,10 @@ pub struct SceneInteractiveChanged(pub bool); #[tauri_specta(event_name = "app-data-refreshed")] pub struct AppDataRefreshed(pub UserData); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "app-state-changed")] +pub struct AppStateChanged(pub AppState); + #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[tauri_specta(event_name = "active-doll-sprite-changed")] pub struct ActiveDollSpriteChanged(pub Option); diff --git a/src-tauri/src/services/app_state.rs b/src-tauri/src/services/app_state.rs new file mode 100644 index 0000000..3feaff1 --- /dev/null +++ b/src-tauri/src/services/app_state.rs @@ -0,0 +1,42 @@ +use std::sync::{Arc, LazyLock, RwLock}; + +use tauri_specta::Event as _; +use tracing::warn; + +use crate::{ + get_app_handle, + models::app_state::{AppState, NekoPosition}, + services::app_events::AppStateChanged, +}; + +static APP_STATE: LazyLock>> = + LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); + +pub fn get_snapshot() -> AppState { + let guard = APP_STATE.read().expect("app state lock poisoned"); + guard.clone() +} + +pub fn set_scene_setup_nekos_position(nekos_position: Option) { + let mut guard = APP_STATE.write().expect("app state lock poisoned"); + guard.scene_setup.nekos_position = nekos_position; + emit_snapshot(&guard); +} + +pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) { + let mut guard = APP_STATE.write().expect("app state lock poisoned"); + guard.scene_setup.nekos_opacity = nekos_opacity.clamp(0.1, 1.0); + emit_snapshot(&guard); +} + +pub fn set_scene_setup_nekos_scale(nekos_scale: f32) { + let mut guard = APP_STATE.write().expect("app state lock poisoned"); + guard.scene_setup.nekos_scale = nekos_scale.clamp(0.5, 2.0); + emit_snapshot(&guard); +} + +fn emit_snapshot(app_state: &AppState) { + if let Err(error) = AppStateChanged(app_state.clone()).emit(get_app_handle()) { + warn!("Failed to emit app-state-changed event: {}", error); + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 7ac78eb..fdde507 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,5 +1,6 @@ pub mod app_data; pub mod app_events; +pub mod app_state; pub mod app_menu; pub mod app_update; pub mod accelerators; diff --git a/src/assets/icons/image.svelte b/src/assets/icons/image.svelte new file mode 100644 index 0000000..c3fac4b --- /dev/null +++ b/src/assets/icons/image.svelte @@ -0,0 +1,17 @@ + diff --git a/src/events/app-state.ts b/src/events/app-state.ts new file mode 100644 index 0000000..b801ac0 --- /dev/null +++ b/src/events/app-state.ts @@ -0,0 +1,32 @@ +import { writable } from "svelte/store"; +import { + commands, + events, + type AppState, + type NekoPosition, +} from "$lib/bindings"; +import { createEventSource } from "./listener-utils"; + +export type NeksPosition = NekoPosition; +export type { AppState }; + +const initialState: AppState = { + sceneSetup: { + nekosPosition: null, + nekosOpacity: 1, + nekosScale: 1, + }, +}; + +export const appState = writable(initialState); + +export const { start: startAppState, stop: stopAppState } = createEventSource( + async (addEventListener) => { + appState.set(await commands.getAppState()); + addEventListener( + await events.appStateChanged.listen((event) => { + appState.set(event.payload); + }), + ); + }, +); diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts index d2bde0b..c4f31cd 100644 --- a/src/lib/bindings.ts +++ b/src/lib/bindings.ts @@ -8,6 +8,9 @@ export const commands = { async getAppData() : Promise { return await TAURI_INVOKE("get_app_data"); }, +async getAppState() : Promise { + return await TAURI_INVOKE("get_app_state"); +}, async getActiveDollSpriteBase64() : Promise { return await TAURI_INVOKE("get_active_doll_sprite_base64"); }, @@ -118,6 +121,15 @@ async sendInteractionCmd(dto: SendInteractionDto) : Promise { }, async getModules() : Promise { return await TAURI_INVOKE("get_modules"); +}, +async setSceneSetupNekosPosition(nekosPosition: NekoPosition | null) : Promise { + await TAURI_INVOKE("set_scene_setup_nekos_position", { nekosPosition }); +}, +async setSceneSetupNekosOpacity(nekosOpacity: number) : Promise { + await TAURI_INVOKE("set_scene_setup_nekos_opacity", { nekosOpacity }); +}, +async setSceneSetupNekosScale(nekosScale: number) : Promise { + await TAURI_INVOKE("set_scene_setup_nekos_scale", { nekosScale }); } } @@ -127,6 +139,7 @@ async getModules() : Promise { export const events = __makeEvents__<{ activeDollSpriteChanged: ActiveDollSpriteChanged, appDataRefreshed: AppDataRefreshed, +appStateChanged: AppStateChanged, authFlowUpdated: AuthFlowUpdated, createDoll: CreateDoll, cursorMoved: CursorMoved, @@ -148,6 +161,7 @@ userStatusChanged: UserStatusChanged }>({ activeDollSpriteChanged: "active-doll-sprite-changed", appDataRefreshed: "app-data-refreshed", +appStateChanged: "app-state-changed", authFlowUpdated: "auth-flow-updated", createDoll: "create-doll", cursorMoved: "cursor-moved", @@ -180,6 +194,8 @@ export type AcceleratorModifier = "cmd" | "alt" | "ctrl" | "shift" export type ActiveDollSpriteChanged = string | null export type AppConfig = { api_base_url: string | null; debug_mode: boolean; accelerators?: Partial<{ [key in AcceleratorAction]: KeyboardAccelerator }> } export type AppDataRefreshed = UserData +export type AppState = { sceneSetup: SceneSetup } +export type AppStateChanged = AppState export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled" export type AuthFlowUpdated = AuthFlowUpdatedPayload export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null } @@ -217,9 +233,11 @@ export type InteractionPayloadDto = { senderUserId: string; senderName: string; export type InteractionReceived = InteractionPayloadDto export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null } export type ModuleMetadata = { id: string; name: string; version: string; description: string | null } +export type NekoPosition = "top-left" | "top" | "top-right" | "left" | "right" | "bottom-left" | "bottom" | "bottom-right" export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null } export type SceneData = { display: DisplayData; grid_size: number } export type SceneInteractiveChanged = boolean +export type SceneSetup = { nekosPosition: NekoPosition | null; nekosOpacity: number; nekosScale: number } export type SendFriendRequestDto = { receiverId: string } export type SendInteractionDto = { recipientUserId: string; content: string; type: string } export type SetInteractionOverlay = boolean diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d681b81..743211b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -15,6 +15,7 @@ stopFriendActiveDollSprite, } from "../events/friend-active-doll-sprite"; import { startAppData } from "../events/app-data"; + import { startAppState, stopAppState } from "../events/app-state"; import { startInteraction, stopInteraction } from "../events/interaction"; import { startSceneInteractive, @@ -27,6 +28,7 @@ onMount(async () => { try { await startAppData(); + await startAppState(); await startActiveDollSprite(); await startFriendActiveDollSprite(); await startCursorTracking(); @@ -47,6 +49,7 @@ stopSceneInteractive(); stopInteraction(); stopUserStatus(); + stopAppState(); }); } diff --git a/src/routes/app-menu/+page.svelte b/src/routes/app-menu/+page.svelte index 8fd8a70..924f8b0 100644 --- a/src/routes/app-menu/+page.svelte +++ b/src/routes/app-menu/+page.svelte @@ -9,6 +9,8 @@ import Users from "../../assets/icons/users.svelte"; import Settings from "../../assets/icons/settings.svelte"; import Blocks from "../../assets/icons/blocks.svelte"; + import Image from "../../assets/icons/image.svelte"; + import Scene from "./tabs/scene/scene.svelte"; let showInteractionOverlay = false; @@ -46,9 +48,19 @@ +
+ +
+ +
+ +
+ +