From 4d7e97771a0c1b99b42e7483059ac0b7de02512d Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sat, 7 Mar 2026 18:36:51 +0800 Subject: [PATCH] migrate from `ts-rs` to `tauri-specta` --- AGENTS.md | 41 +-- README.md | 12 - src-tauri/Cargo.lock | 120 ++++++-- src-tauri/Cargo.toml | 4 +- src-tauri/src/commands/app.rs | 3 + src-tauri/src/commands/app_state.rs | 3 + src-tauri/src/commands/auth.rs | 5 + src-tauri/src/commands/config.rs | 3 + src-tauri/src/commands/dolls.rs | 7 + src-tauri/src/commands/friends.rs | 8 + src-tauri/src/commands/interaction.rs | 1 + src-tauri/src/commands/petpet.rs | 1 + src-tauri/src/commands/sprite.rs | 1 + src-tauri/src/lib.rs | 53 +++- src-tauri/src/models/app_data.rs | 11 +- src-tauri/src/models/dolls.rs | 17 +- src-tauri/src/models/event_payloads.rs | 29 +- src-tauri/src/models/friends.rs | 14 +- src-tauri/src/models/health.rs | 5 +- src-tauri/src/models/interaction.rs | 11 +- src-tauri/src/models/user.rs | 5 +- src-tauri/src/services/app_events.rs | 167 +++++------ .../src/services/client_config_manager.rs | 3 +- src-tauri/src/services/cursor.rs | 19 +- src-tauri/src/services/doll_editor.rs | 25 +- .../src/services/presence_modules/models.rs | 8 +- src-tauri/src/services/scene.rs | 10 +- src-tauri/src/services/ws/emitter.rs | 12 +- src-tauri/src/services/ws/friend.rs | 22 +- src-tauri/src/services/ws/interaction.rs | 6 +- src-tauri/src/services/ws/mod.rs | 1 + src-tauri/src/services/ws/types.rs | 3 +- src-tauri/src/services/ws/user_status.rs | 7 +- src-tauri/src/state/ui.rs | 8 +- src/events/app-data.ts | 16 +- src/events/cursor.ts | 14 +- src/events/friend-cursor.ts | 30 +- src/events/interaction.ts | 21 +- src/events/scene-interactive.ts | 15 +- src/events/user-status.ts | 45 ++- src/lib/bindings.ts | 281 ++++++++++++++++++ src/lib/utils/sprite-utils.ts | 12 +- src/routes/app-menu/+page.svelte | 5 +- src/routes/app-menu/tabs/friends.svelte | 47 ++- src/routes/app-menu/tabs/modules.svelte | 5 +- src/routes/app-menu/tabs/preferences.svelte | 18 +- .../tabs/your-dolls/dolls-list.svelte | 3 +- .../app-menu/tabs/your-dolls/index.svelte | 12 +- src/routes/client-config-manager/+page.svelte | 16 +- src/routes/doll-editor/+page.svelte | 16 +- src/routes/health-manager/+page.svelte | 6 +- src/routes/scene/+page.svelte | 7 +- src/routes/scene/components/debug-bar.svelte | 3 +- src/routes/welcome/+page.svelte | 21 +- src/types/bindings/AppEvents.ts | 3 - src/types/bindings/AppEventsConstants.ts | 24 -- src/types/bindings/CreateDollDto.ts | 4 - src/types/bindings/CursorPosition.ts | 3 - src/types/bindings/CursorPositions.ts | 4 - src/types/bindings/DisplayData.ts | 3 - src/types/bindings/DollColorSchemeDto.ts | 3 - src/types/bindings/DollConfigurationDto.ts | 4 - src/types/bindings/DollDto.ts | 4 - .../FriendActiveDollChangedPayload.ts | 4 - .../bindings/FriendDisconnectedPayload.ts | 3 - .../bindings/FriendRequestAcceptedPayload.ts | 4 - .../bindings/FriendRequestDeniedPayload.ts | 4 - .../bindings/FriendRequestReceivedPayload.ts | 4 - .../bindings/FriendRequestResponseDto.ts | 4 - src/types/bindings/FriendUserStatusPayload.ts | 4 - src/types/bindings/FriendshipResponseDto.ts | 4 - src/types/bindings/HealthResponseDto.ts | 3 - .../bindings/InteractionDeliveryFailedDto.ts | 3 - src/types/bindings/InteractionPayloadDto.ts | 3 - src/types/bindings/ModuleMetadata.ts | 3 - src/types/bindings/PresenceStatus.ts | 3 - src/types/bindings/SceneData.ts | 4 - src/types/bindings/SendFriendRequestDto.ts | 3 - src/types/bindings/SendInteractionDto.ts | 3 - src/types/bindings/UnfriendedPayload.ts | 3 - src/types/bindings/UpdateDollDto.ts | 4 - src/types/bindings/UserBasicDto.ts | 4 - src/types/bindings/UserData.ts | 7 - src/types/bindings/UserProfile.ts | 3 - src/types/bindings/UserStatusPayload.ts | 5 - src/types/bindings/UserStatusState.ts | 3 - 86 files changed, 766 insertions(+), 609 deletions(-) create mode 100644 src/lib/bindings.ts delete mode 100644 src/types/bindings/AppEvents.ts delete mode 100644 src/types/bindings/AppEventsConstants.ts delete mode 100644 src/types/bindings/CreateDollDto.ts delete mode 100644 src/types/bindings/CursorPosition.ts delete mode 100644 src/types/bindings/CursorPositions.ts delete mode 100644 src/types/bindings/DisplayData.ts delete mode 100644 src/types/bindings/DollColorSchemeDto.ts delete mode 100644 src/types/bindings/DollConfigurationDto.ts delete mode 100644 src/types/bindings/DollDto.ts delete mode 100644 src/types/bindings/FriendActiveDollChangedPayload.ts delete mode 100644 src/types/bindings/FriendDisconnectedPayload.ts delete mode 100644 src/types/bindings/FriendRequestAcceptedPayload.ts delete mode 100644 src/types/bindings/FriendRequestDeniedPayload.ts delete mode 100644 src/types/bindings/FriendRequestReceivedPayload.ts delete mode 100644 src/types/bindings/FriendRequestResponseDto.ts delete mode 100644 src/types/bindings/FriendUserStatusPayload.ts delete mode 100644 src/types/bindings/FriendshipResponseDto.ts delete mode 100644 src/types/bindings/HealthResponseDto.ts delete mode 100644 src/types/bindings/InteractionDeliveryFailedDto.ts delete mode 100644 src/types/bindings/InteractionPayloadDto.ts delete mode 100644 src/types/bindings/ModuleMetadata.ts delete mode 100644 src/types/bindings/PresenceStatus.ts delete mode 100644 src/types/bindings/SceneData.ts delete mode 100644 src/types/bindings/SendFriendRequestDto.ts delete mode 100644 src/types/bindings/SendInteractionDto.ts delete mode 100644 src/types/bindings/UnfriendedPayload.ts delete mode 100644 src/types/bindings/UpdateDollDto.ts delete mode 100644 src/types/bindings/UserBasicDto.ts delete mode 100644 src/types/bindings/UserData.ts delete mode 100644 src/types/bindings/UserProfile.ts delete mode 100644 src/types/bindings/UserStatusPayload.ts delete mode 100644 src/types/bindings/UserStatusState.ts diff --git a/AGENTS.md b/AGENTS.md index 63dc258..0f4a9cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,56 +6,35 @@ Passive social app connecting peers through mouse cursor interactions in the for Desktop client app for Friendolls. -## Build/Lint/Test Commands +## Commands -### Full App (Standard) +Check code integrity after every significant change: -- **Dev**: `pnpm dev` (runs Tauri dev mode) +- `cd src-tauri && cargo check` for Rust local backend +- `pnpm check` for Svelte frontend -### Frontend (SvelteKit + TypeScript) +Generate TypeScript bindings with `tauri-specta` when new tauri commands, events or Rust models is added / modified: -- **Build**: `pnpm build` -- **Dev server**: `pnpm dev` -- **Type check**: `svelte-kit sync && svelte-check --tsconfig ./tsconfig.json` -- **Watch type check**: `svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch` - -### Backend (Rust + Tauri) - -- **Build**: `cargo build` -- **Check**: `cargo check` -- **Lint**: `cargo clippy` -- **Test**: `cargo test` -- **Run single test**: `cargo test ` -- **Generate TypeScript bindings (from project root)**: `TS_RS_EXPORT_DIR="../src/types/bindings" cargo test export_bindings --manifest-path="./src-tauri/Cargo.toml"` - -## Code Style Guidelines +- `timeout 30 pnpm tauri dev` ### TypeScript/Svelte -- **Strict TypeScript**: `"strict": true` enabled -- **Imports**: At top of file, before exports -- **Naming**: camelCase for variables/functions, PascalCase for types/interfaces -- **Modules**: ES modules only -- **Styling**: TailwindCSS + DaisyUI - **Framework**: SvelteKit in SPA mode (SSR disabled for Tauri) -- **Error handling**: Standard try/catch with console.error logging -- **Responsibility**: Minimal logic & data handling, should play as stateless dumb client +- **Styling**: TailwindCSS + DaisyUI +- **Responsibility**: Minimal logic & data handling, should play as stateless dumb client, communicate with Rust local backend via Tauri events ### Rust - **Error handling**: `thiserror::Error` derive with descriptive error messages - **Logging**: `tracing` crate for structured logging (info/warn/error) - **Async**: `tokio` runtime with `async`/`await` -- **Serialization**: `serde` with `Serialize`/`Deserialize` - **Naming**: snake_case for functions/variables, PascalCase for types/structs -- **Documentation**: Comprehensive doc comments with examples for public APIs - **State management**: Custom macros (`lock_r!`/`lock_w!`) for thread-safe access -- **Security**: Use secure storage (keyring) for sensitive data, proper PKCE flow for OAuth -- **Imports**: Group by standard library, then external crates, then local modules +- **Security**: Use secure storage (keyring) for sensitive data - **Responsibility**: Handles app state & data, business logic, controls UI via events. ## Note Be sure to gather sufficient context from codebase before proceeding with changes. Observe patterns and follow trends. -Do not run the app yourself. `cd src-tauri && cargo check` & `pnpm check` to confirm your changes are error-free. Don't perform git actions yourself. +Do not run the app without timeout. `cd src-tauri && cargo check` & `pnpm check` to confirm your changes are error-free. Don't perform git actions yourself. diff --git a/README.md b/README.md index d6edb19..79d6e3d 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,4 @@ This repository contins source for Friendolls desktop app. Will add more info when the app scales. -Run the following command in project root after changes to models on Rust side to generate TypeScript type bindings from Rust models - -```sh -# average unix shells -TS_RS_EXPORT_DIR="../src/types/bindings" cargo test export_bindings --manifest-path="./src-tauri/Cargo.toml" -``` - -```sh -# powershell -$Env:TS_RS_EXPORT_DIR = "../src/types/bindings"; cargo test export_bindings --manifest-path="./src-tauri/Cargo.toml" -``` - > _To the gods of programming, please grant me the perseverance to push through and get this app into production_ 🙏 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9f6e7cb..186c8f6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "adler2" version = "2.0.1" @@ -1227,6 +1233,8 @@ dependencies = [ "serde", "serde_json", "sha2", + "specta", + "specta-typescript", "strum", "tauri", "tauri-build", @@ -1235,13 +1243,13 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-positioner", "tauri-plugin-process", + "tauri-specta", "thiserror 1.0.69", "tokio", "tokio-util", "tracing", "tracing-appender", "tracing-subscriber", - "ts-rs", "url", "windows 0.58.0", ] @@ -3035,6 +3043,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -4237,6 +4251,50 @@ dependencies = [ "system-deps", ] +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" +dependencies = [ + "paste", + "specta-macros", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63" +dependencies = [ + "specta", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d" +dependencies = [ + "specta", + "specta-serde", + "thiserror 1.0.69", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -4487,6 +4545,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", "tauri-build", "tauri-macros", @@ -4737,6 +4796,34 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.17", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "tauri-utils" version = "2.8.0" @@ -4810,15 +4897,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -5225,28 +5303,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ts-rs" -version = "11.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424" -dependencies = [ - "thiserror 2.0.17", - "ts-rs-macros", -] - -[[package]] -name = "ts-rs-macros" -version = "11.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", - "termcolor", -] - [[package]] name = "tungstenite" version = "0.21.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c18a4a2..af2df4b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,7 +24,9 @@ tauri-plugin-positioner = "2" tauri-plugin-process = "2" reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] } tokio-util = "0.7" -ts-rs = "11.0.1" +specta = "=2.0.0-rc.22" +specta-typescript = "0.0.9" +tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } strum = { version = "0.26", features = ["derive"] } device_query = "4.0.1" dotenvy = "0.15.7" diff --git a/src-tauri/src/commands/app.rs b/src-tauri/src/commands/app.rs index b730b12..1897f29 100644 --- a/src-tauri/src/commands/app.rs +++ b/src-tauri/src/commands/app.rs @@ -4,6 +4,7 @@ use crate::services::auth::get_session_token; use tracing::info; #[tauri::command] +#[specta::specta] pub fn quit_app() -> Result<(), String> { let app_handle = get_app_handle(); app_handle.exit(0); @@ -11,6 +12,7 @@ pub fn quit_app() -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn restart_app() { let app_handle = get_app_handle(); app_handle.restart(); @@ -21,6 +23,7 @@ pub fn restart_app() { /// Validates server health, checks for a valid session token, /// then reconstructs the user session (re-fetches app data + WebSocket). #[tauri::command] +#[specta::specta] pub async fn retry_connection() -> Result<(), String> { info!("Retrying connection..."); diff --git a/src-tauri/src/commands/app_state.rs b/src-tauri/src/commands/app_state.rs index 20dbe9e..a6838cd 100644 --- a/src-tauri/src/commands/app_state.rs +++ b/src-tauri/src/commands/app_state.rs @@ -6,12 +6,14 @@ use crate::{ }; #[tauri::command] +#[specta::specta] pub fn get_app_data() -> Result { let guard = lock_r!(FDOLL); Ok(guard.user_data.clone()) } #[tauri::command] +#[specta::specta] pub async fn refresh_app_data() -> Result { init_app_data_scoped(AppDataRefreshScope::All).await; let guard = lock_r!(FDOLL); @@ -19,6 +21,7 @@ pub async fn refresh_app_data() -> Result { } #[tauri::command] +#[specta::specta] pub fn get_modules() -> Result, String> { let guard = lock_r!(FDOLL); Ok(guard.modules.metadatas.clone()) diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index 56ef8fc..db286c3 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -1,11 +1,13 @@ use crate::services::auth; #[tauri::command] +#[specta::specta] pub async fn logout_and_restart() -> Result<(), String> { auth::logout_and_restart().await.map_err(|e| e.to_string()) } #[tauri::command] +#[specta::specta] pub async fn login(email: String, password: String) -> Result<(), String> { auth::login_and_init_session(&email, &password) .await @@ -13,6 +15,7 @@ pub async fn login(email: String, password: String) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub async fn register( email: String, password: String, @@ -30,6 +33,7 @@ pub async fn register( } #[tauri::command] +#[specta::specta] pub async fn change_password( current_password: String, new_password: String, @@ -40,6 +44,7 @@ pub async fn change_password( } #[tauri::command] +#[specta::specta] pub async fn reset_password(old_password: String, new_password: String) -> Result<(), String> { auth::reset_password(&old_password, &new_password) .await diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index fe8c995..ffbc56f 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -7,6 +7,7 @@ use crate::{ }; #[tauri::command] +#[specta::specta] pub fn get_client_config() -> AppConfig { let mut guard = lock_w!(FDOLL); guard.app_config = load_app_config(); @@ -14,6 +15,7 @@ pub fn get_client_config() -> AppConfig { } #[tauri::command] +#[specta::specta] pub fn save_client_config(config: AppConfig) -> Result<(), String> { match save_app_config(config) { Ok(saved) => { @@ -26,6 +28,7 @@ pub fn save_client_config(config: AppConfig) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub async fn open_client_config_manager() -> Result<(), String> { open_config_manager_window().map_err(|e| e.to_string()) } diff --git a/src-tauri/src/commands/dolls.rs b/src-tauri/src/commands/dolls.rs index 1b5c8a4..6da5e68 100644 --- a/src-tauri/src/commands/dolls.rs +++ b/src-tauri/src/commands/dolls.rs @@ -9,6 +9,7 @@ use crate::{ }; #[tauri::command] +#[specta::specta] pub async fn get_dolls() -> Result, String> { DollsRemote::new() .get_dolls() @@ -17,6 +18,7 @@ pub async fn get_dolls() -> Result, String> { } #[tauri::command] +#[specta::specta] pub async fn get_doll(id: String) -> Result { DollsRemote::new() .get_doll(&id) @@ -25,6 +27,7 @@ pub async fn get_doll(id: String) -> Result { } #[tauri::command] +#[specta::specta] pub async fn create_doll(dto: CreateDollDto) -> Result { let result = DollsRemote::new() .create_doll(dto) @@ -37,6 +40,7 @@ pub async fn create_doll(dto: CreateDollDto) -> Result { } #[tauri::command] +#[specta::specta] pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result { let result = DollsRemote::new() .update_doll(&id, dto) @@ -55,6 +59,7 @@ pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result Result<(), String> { DollsRemote::new() .delete_doll(&id) @@ -73,6 +78,7 @@ pub async fn delete_doll(id: String) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub async fn set_active_doll(doll_id: String) -> Result<(), String> { UserRemote::new() .set_active_doll(&doll_id) @@ -85,6 +91,7 @@ pub async fn set_active_doll(doll_id: String) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub async fn remove_active_doll() -> Result<(), String> { UserRemote::new() .remove_active_doll() diff --git a/src-tauri/src/commands/friends.rs b/src-tauri/src/commands/friends.rs index 2ce1764..5653a9d 100644 --- a/src-tauri/src/commands/friends.rs +++ b/src-tauri/src/commands/friends.rs @@ -6,6 +6,7 @@ use crate::state::AppDataRefreshScope; use crate::commands::refresh_app_data; #[tauri::command] +#[specta::specta] pub async fn list_friends() -> Result, String> { FriendRemote::new() .get_friends() @@ -14,6 +15,7 @@ pub async fn list_friends() -> Result, String> { } #[tauri::command] +#[specta::specta] pub async fn search_users(username: Option) -> Result, String> { tracing::info!( "Tauri command search_users called with username: {:?}", @@ -30,6 +32,7 @@ pub async fn search_users(username: Option) -> Result, } #[tauri::command] +#[specta::specta] pub async fn send_friend_request( request: SendFriendRequestDto, ) -> Result { @@ -44,6 +47,7 @@ pub async fn send_friend_request( } #[tauri::command] +#[specta::specta] pub async fn received_friend_requests() -> Result, String> { FriendRemote::new() .get_received_requests() @@ -52,6 +56,7 @@ pub async fn received_friend_requests() -> Result, } #[tauri::command] +#[specta::specta] pub async fn sent_friend_requests() -> Result, String> { FriendRemote::new() .get_sent_requests() @@ -60,6 +65,7 @@ pub async fn sent_friend_requests() -> Result, Str } #[tauri::command] +#[specta::specta] pub async fn accept_friend_request(request_id: String) -> Result { let result = FriendRemote::new() .accept_friend_request(&request_id) @@ -72,6 +78,7 @@ pub async fn accept_friend_request(request_id: String) -> Result Result { let result = FriendRemote::new() .deny_friend_request(&request_id) @@ -84,6 +91,7 @@ pub async fn deny_friend_request(request_id: String) -> Result Result<(), String> { FriendRemote::new() .unfriend(&friend_id) diff --git a/src-tauri/src/commands/interaction.rs b/src-tauri/src/commands/interaction.rs index f9f60ac..d543c1f 100644 --- a/src-tauri/src/commands/interaction.rs +++ b/src-tauri/src/commands/interaction.rs @@ -1,6 +1,7 @@ use crate::{models::interaction::SendInteractionDto, services::interaction::send_interaction}; #[tauri::command] +#[specta::specta] pub async fn send_interaction_cmd(dto: SendInteractionDto) -> Result<(), String> { send_interaction(dto).await } diff --git a/src-tauri/src/commands/petpet.rs b/src-tauri/src/commands/petpet.rs index 602c8e0..e42ce94 100644 --- a/src-tauri/src/commands/petpet.rs +++ b/src-tauri/src/commands/petpet.rs @@ -2,6 +2,7 @@ use crate::models::dolls::DollDto; use crate::services::petpet; #[tauri::command] +#[specta::specta] pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result { petpet::encode_pet_doll_gif_base64(doll) } diff --git a/src-tauri/src/commands/sprite.rs b/src-tauri/src/commands/sprite.rs index 9da0698..07b4162 100644 --- a/src-tauri/src/commands/sprite.rs +++ b/src-tauri/src/commands/sprite.rs @@ -1,6 +1,7 @@ use crate::services::sprite_recolor; #[tauri::command] +#[specta::specta] pub fn recolor_gif_base64( white_color_hex: String, black_color_hex: String, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 191ca40..ce7a759 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,7 +19,17 @@ use commands::friends::{ use commands::interaction::send_interaction_cmd; use commands::sprite::recolor_gif_base64; use commands::petpet::encode_pet_doll_gif_base64; +use specta_typescript::Typescript; use tauri::async_runtime; +use tauri_specta::{Builder as SpectaBuilder, ErrorHandlingMode, collect_commands, collect_events}; + +use crate::services::app_events::{ + AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged, + FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted, + FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, + InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, + SetInteractionOverlay, Unfriended, UserStatusChanged, +}; static APP_HANDLE: std::sync::OnceLock> = std::sync::OnceLock::new(); @@ -51,12 +61,9 @@ fn register_app_events(event: tauri::RunEvent) { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_positioner::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_process::init()) - .invoke_handler(tauri::generate_handler![ + let specta_builder = SpectaBuilder::::new() + .error_handling(ErrorHandlingMode::Throw) + .commands(collect_commands![ get_app_data, refresh_app_data, list_friends, @@ -94,7 +101,39 @@ pub fn run() { send_interaction_cmd, get_modules ]) - .setup(|app| { + .events(collect_events![ + CursorMoved, + SceneInteractiveChanged, + AppDataRefreshed, + SetInteractionOverlay, + EditDoll, + CreateDoll, + UserStatusChanged, + FriendCursorPositionUpdated, + FriendDisconnected, + FriendActiveDollChanged, + FriendUserStatusChanged, + InteractionReceived, + InteractionDeliveryFailed, + FriendRequestReceived, + FriendRequestAccepted, + FriendRequestDenied, + Unfriended + ]); + + #[cfg(debug_assertions)] + specta_builder + .export(Typescript::default(), "../src/lib/bindings.ts") + .expect("Failed to export TypeScript bindings"); + + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_positioner::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_process::init()) + .invoke_handler(specta_builder.invoke_handler()) + .setup(move |app| { + specta_builder.mount_events(app); APP_HANDLE .set(app.handle().to_owned()) .expect("Failed to init app handle."); diff --git a/src-tauri/src/models/app_data.rs b/src-tauri/src/models/app_data.rs index 4e0488e..4ad5792 100644 --- a/src-tauri/src/models/app_data.rs +++ b/src-tauri/src/models/app_data.rs @@ -1,10 +1,9 @@ use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use specta::Type; use crate::models::{dolls::DollDto, friends::FriendshipResponseDto, user::UserProfile}; -#[derive(Serialize, Deserialize, Clone, Debug, TS)] -#[ts(export)] +#[derive(Serialize, Deserialize, Clone, Debug, Type)] pub struct DisplayData { pub screen_width: i32, pub screen_height: i32, @@ -21,8 +20,7 @@ impl Default for DisplayData { } } -#[derive(Serialize, Deserialize, Clone, Debug, TS)] -#[ts(export)] +#[derive(Serialize, Deserialize, Clone, Debug, Type)] pub struct SceneData { pub display: DisplayData, pub grid_size: i32, @@ -37,8 +35,7 @@ impl Default for SceneData { } } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] -#[ts(export)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] pub struct UserData { pub user: Option, pub friends: Option>, diff --git a/src-tauri/src/models/dolls.rs b/src-tauri/src/models/dolls.rs index 45b3e93..f45e46c 100644 --- a/src-tauri/src/models/dolls.rs +++ b/src-tauri/src/models/dolls.rs @@ -1,40 +1,35 @@ use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use specta::Type; -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct DollColorSchemeDto { pub outline: String, pub body: String, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct DollConfigurationDto { pub color_scheme: DollColorSchemeDto, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct CreateDollDto { pub name: String, pub configuration: Option, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct UpdateDollDto { pub name: Option, pub configuration: Option, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct DollDto { pub id: String, pub name: String, diff --git a/src-tauri/src/models/event_payloads.rs b/src-tauri/src/models/event_payloads.rs index 39800d4..5762383 100644 --- a/src-tauri/src/models/event_payloads.rs +++ b/src-tauri/src/models/event_payloads.rs @@ -1,79 +1,70 @@ use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use specta::Type; use super::dolls::DollDto; use super::friends::UserBasicDto; use crate::services::presence_modules::models::PresenceStatus; -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "lowercase")] -#[ts(export)] pub enum UserStatusState { Idle, Resting, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct UserStatusPayload { pub presence_status: PresenceStatus, pub state: UserStatusState, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendUserStatusPayload { pub user_id: String, pub status: UserStatusPayload, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendDisconnectedPayload { pub user_id: String, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendActiveDollChangedPayload { pub friend_id: String, pub doll: Option, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendRequestReceivedPayload { pub id: String, pub sender: UserBasicDto, pub created_at: String, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendRequestAcceptedPayload { pub id: String, pub friend: UserBasicDto, pub accepted_at: String, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendRequestDeniedPayload { pub id: String, pub denier: UserBasicDto, pub denied_at: String, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct UnfriendedPayload { pub friend_id: String, } diff --git a/src-tauri/src/models/friends.rs b/src-tauri/src/models/friends.rs index 9f91dc9..f42ac26 100644 --- a/src-tauri/src/models/friends.rs +++ b/src-tauri/src/models/friends.rs @@ -1,11 +1,10 @@ use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use specta::Type; use super::dolls::DollDto; -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct UserBasicDto { pub id: String, pub name: String, @@ -13,25 +12,22 @@ pub struct UserBasicDto { pub active_doll: Option, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendshipResponseDto { pub id: String, pub friend: Option, pub created_at: String, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct SendFriendRequestDto { pub receiver_id: String, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct FriendRequestResponseDto { pub id: String, pub sender: UserBasicDto, diff --git a/src-tauri/src/models/health.rs b/src-tauri/src/models/health.rs index ce55c44..114472a 100644 --- a/src-tauri/src/models/health.rs +++ b/src-tauri/src/models/health.rs @@ -1,11 +1,10 @@ use reqwest::StatusCode; use serde::{Deserialize, Serialize}; +use specta::Type; use thiserror::Error; -use ts_rs::TS; -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct HealthResponseDto { pub status: String, pub version: String, diff --git a/src-tauri/src/models/interaction.rs b/src-tauri/src/models/interaction.rs index 54d1372..1c1b400 100644 --- a/src-tauri/src/models/interaction.rs +++ b/src-tauri/src/models/interaction.rs @@ -1,8 +1,7 @@ use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use specta::Type; -#[derive(Clone, Serialize, Deserialize, Debug, TS)] -#[ts(export)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] pub struct SendInteractionDto { pub recipient_user_id: String, @@ -11,8 +10,7 @@ pub struct SendInteractionDto { pub type_: String, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] -#[ts(export)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] pub struct InteractionPayloadDto { pub sender_user_id: String, @@ -23,8 +21,7 @@ pub struct InteractionPayloadDto { pub timestamp: String, } -#[derive(Clone, Serialize, Deserialize, Debug, TS)] -#[ts(export)] +#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[serde(rename_all = "camelCase")] pub struct InteractionDeliveryFailedDto { pub recipient_user_id: String, diff --git a/src-tauri/src/models/user.rs b/src-tauri/src/models/user.rs index b57ff3e..fc26bb6 100644 --- a/src-tauri/src/models/user.rs +++ b/src-tauri/src/models/user.rs @@ -1,9 +1,8 @@ use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use specta::Type; -#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct UserProfile { pub id: String, pub name: String, diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index 147db06..2f6ccbb 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -1,105 +1,84 @@ -use serde::Serialize; -#[allow(unused_imports)] -use std::{fs, path::Path}; -use strum::{AsRefStr, EnumIter}; -use ts_rs::TS; +use serde::{Deserialize, Serialize}; +use specta::Type; +use tauri_specta::Event; -#[derive(Serialize, TS, EnumIter, AsRefStr)] -#[serde(rename_all = "kebab-case")] -#[ts(export)] -pub enum AppEvents { - CursorPosition, - SceneInteractive, - AppDataRefreshed, - SetInteractionOverlay, - EditDoll, - CreateDoll, - UserStatusChanged, - FriendCursorPosition, - FriendDisconnected, - FriendActiveDollChanged, - FriendUserStatus, - InteractionReceived, - InteractionDeliveryFailed, - FriendRequestReceived, - FriendRequestAccepted, - FriendRequestDenied, - Unfriended, -} +use crate::{ + models::{ + app_data::UserData, + event_payloads::{ + FriendActiveDollChangedPayload, FriendDisconnectedPayload, + FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload, + FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload, + }, + interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}, + }, + services::{cursor::CursorPositions, ws::OutgoingFriendCursorPayload}, +}; -impl AppEvents { - pub fn as_str(&self) -> &'static str { - match self { - AppEvents::CursorPosition => "cursor-position", - AppEvents::SceneInteractive => "scene-interactive", - AppEvents::AppDataRefreshed => "app-data-refreshed", - AppEvents::SetInteractionOverlay => "set-interaction-overlay", - AppEvents::EditDoll => "edit-doll", - AppEvents::CreateDoll => "create-doll", - AppEvents::UserStatusChanged => "user-status-changed", - AppEvents::FriendCursorPosition => "friend-cursor-position", - AppEvents::FriendDisconnected => "friend-disconnected", - AppEvents::FriendActiveDollChanged => "friend-active-doll-changed", - AppEvents::FriendUserStatus => "friend-user-status", - AppEvents::InteractionReceived => "interaction-received", - AppEvents::InteractionDeliveryFailed => "interaction-delivery-failed", - AppEvents::FriendRequestReceived => "friend-request-received", - AppEvents::FriendRequestAccepted => "friend-request-accepted", - AppEvents::FriendRequestDenied => "friend-request-denied", - AppEvents::Unfriended => "unfriended", - } - } -} +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "cursor-position")] +pub struct CursorMoved(pub CursorPositions); -#[test] -fn export_bindings_appeventsconsts() { - use strum::IntoEnumIterator; +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "scene-interactive")] +pub struct SceneInteractiveChanged(pub bool); - let some_export_dir = std::env::var("TS_RS_EXPORT_DIR") - .ok() - .map(|s| Path::new(&s).to_owned()); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "app-data-refreshed")] +pub struct AppDataRefreshed(pub UserData); - let Some(export_dir) = some_export_dir else { - eprintln!("TS_RS_EXPORT_DIR not set, skipping constants export"); - return; - }; +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "set-interaction-overlay")] +pub struct SetInteractionOverlay(pub bool); - let to_kebab_case = |s: &str| -> String { - let mut result = String::new(); - for (i, c) in s.chars().enumerate() { - if c.is_uppercase() { - if i > 0 { - result.push('-'); - } - result.push(c.to_lowercase().next().unwrap()); - } else { - result.push(c); - } - } - result - }; +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "edit-doll")] +pub struct EditDoll(pub String); - let mut lines = vec![ - r#"// Auto-generated constants - DO NOT EDIT"#.to_string(), - r#"// Generated from Rust AppEvents enum"#.to_string(), - "".to_string(), - "export const AppEvents = {".to_string(), - ]; +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "create-doll")] +pub struct CreateDoll; - for variant in AppEvents::iter() { - let name = variant.as_ref(); - let kebab = to_kebab_case(name); - lines.push(format!(" {}: \"{}\",", name, kebab)); - } +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "user-status-changed")] +pub struct UserStatusChanged(pub UserStatusPayload); - lines.push("} as const;".to_string()); - lines.push("".to_string()); - lines.push("export type AppEvents = typeof AppEvents[keyof typeof AppEvents];".to_string()); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-cursor-position")] +pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload); - let constants_content = lines.join("\n"); +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-disconnected")] +pub struct FriendDisconnected(pub FriendDisconnectedPayload); - let constants_path = export_dir.join("AppEventsConstants.ts"); - if let Err(e) = fs::write(&constants_path, constants_content) { - eprintln!("Failed to write {}: {}", constants_path.display(), e); - } -} +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-active-doll-changed")] +pub struct FriendActiveDollChanged(pub FriendActiveDollChangedPayload); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-user-status")] +pub struct FriendUserStatusChanged(pub FriendUserStatusPayload); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "interaction-received")] +pub struct InteractionReceived(pub InteractionPayloadDto); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "interaction-delivery-failed")] +pub struct InteractionDeliveryFailed(pub InteractionDeliveryFailedDto); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-request-received")] +pub struct FriendRequestReceived(pub FriendRequestReceivedPayload); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-request-accepted")] +pub struct FriendRequestAccepted(pub FriendRequestAcceptedPayload); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "friend-request-denied")] +pub struct FriendRequestDenied(pub FriendRequestDeniedPayload); + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[tauri_specta(event_name = "unfriended")] +pub struct Unfriended(pub UnfriendedPayload); diff --git a/src-tauri/src/services/client_config_manager.rs b/src-tauri/src/services/client_config_manager.rs index efd571a..93638c6 100644 --- a/src-tauri/src/services/client_config_manager.rs +++ b/src-tauri/src/services/client_config_manager.rs @@ -1,6 +1,7 @@ use std::{fs, path::PathBuf}; use serde::{Deserialize, Serialize}; +use specta::Type; use tauri::Manager; use thiserror::Error; use tracing::{error, warn}; @@ -8,7 +9,7 @@ use url::Url; use crate::get_app_handle; -#[derive(Default, Serialize, Deserialize, Clone, Debug)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] pub struct AppConfig { pub api_base_url: Option, } diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index 27012a1..ac91b93 100644 --- a/src-tauri/src/services/cursor.rs +++ b/src-tauri/src/services/cursor.rs @@ -1,26 +1,29 @@ use device_query::{DeviceEvents, DeviceEventsHandler}; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; +use specta::Type; use std::sync::{Arc, Mutex}; use std::time::Duration; -use tauri::Emitter; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; -use ts_rs::TS; -use crate::{get_app_handle, lock_r, services::app_events::AppEvents, state::FDOLL}; +use crate::{ + get_app_handle, + lock_r, + services::app_events::CursorMoved, + state::FDOLL, +}; +use tauri_specta::Event as _; -#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct CursorPosition { pub x: f64, pub y: f64, } -#[derive(Debug, Clone, Serialize, TS)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct CursorPositions { pub raw: CursorPosition, pub mapped: CursorPosition, @@ -103,7 +106,7 @@ async fn init_cursor_tracking_i() -> Result<(), String> { crate::services::ws::report_cursor_data(mapped_for_ws).await; // 2. Broadcast to local windows - if let Err(e) = app_handle.emit(AppEvents::CursorPosition.as_str(), &positions) { + if let Err(e) = CursorMoved(positions).emit(app_handle) { error!("Failed to emit cursor position event: {:?}", e); } } diff --git a/src-tauri/src/services/doll_editor.rs b/src-tauri/src/services/doll_editor.rs index 5d4afc9..95249b4 100644 --- a/src-tauri/src/services/doll_editor.rs +++ b/src-tauri/src/services/doll_editor.rs @@ -1,9 +1,13 @@ #[cfg(target_os = "windows")] use tauri::WebviewWindow; -use tauri::{Emitter, Listener, Manager}; +use tauri::{Listener, Manager}; +use tauri_specta::Event as _; use tracing::{error, info}; -use crate::{get_app_handle, services::app_events::AppEvents}; +use crate::{ + get_app_handle, + services::app_events::{CreateDoll, EditDoll, SetInteractionOverlay}, +}; static APP_MENU_WINDOW_LABEL: &str = "app_menu"; @@ -51,6 +55,7 @@ fn set_window_interaction(window: &WebviewWindow, enable: bool) { } #[tauri::command] +#[specta::specta] pub async fn open_doll_editor_window(doll_id: Option) { let app_handle = get_app_handle().clone(); @@ -76,17 +81,17 @@ pub async fn open_doll_editor_window(doll_id: Option) { // Ensure overlay is active on parent (redundancy for safety) #[cfg(target_os = "macos")] if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) { - if let Err(e) = parent.emit(AppEvents::SetInteractionOverlay.as_str(), true) { + if let Err(e) = SetInteractionOverlay(true).emit(&parent) { error!("Failed to ensure interaction overlay on parent: {}", e); } } // Emit event to update context if let Some(id) = doll_id { - if let Err(e) = window.emit(AppEvents::EditDoll.as_str(), id) { + if let Err(e) = EditDoll(id).emit(&window) { error!("Failed to emit edit-doll event: {}", e); } - } else if let Err(e) = window.emit(AppEvents::CreateDoll.as_str(), ()) { + } else if let Err(e) = CreateDoll.emit(&window) { error!("Failed to emit create-doll event: {}", e); } @@ -136,7 +141,7 @@ pub async fn open_doll_editor_window(doll_id: Option) { let app_handle_clone = get_app_handle().clone(); // Emit event to show overlay - if let Err(e) = parent.emit(AppEvents::SetInteractionOverlay.as_str(), true) { + if let Err(e) = SetInteractionOverlay(true).emit(&parent) { error!("Failed to emit set-interaction-overlay event: {}", e); } @@ -169,7 +174,7 @@ pub async fn open_doll_editor_window(doll_id: Option) { parent.unlisten(id); } // Remove overlay if we failed - let _ = parent.emit(AppEvents::SetInteractionOverlay.as_str(), false); + let _ = SetInteractionOverlay(false).emit(&parent); } return; } @@ -204,9 +209,7 @@ pub async fn open_doll_editor_window(doll_id: Option) { parent.unlisten(id); } // Remove overlay - if let Err(e) = parent - .emit(AppEvents::SetInteractionOverlay.as_str(), false) - { + if let Err(e) = SetInteractionOverlay(false).emit(&parent) { error!("Failed to remove interaction overlay: {}", e); } } @@ -232,7 +235,7 @@ pub async fn open_doll_editor_window(doll_id: Option) { if let Some(id) = parent_focus_listener_id { parent.unlisten(id); } - let _ = parent.emit(AppEvents::SetInteractionOverlay.as_str(), false); + let _ = SetInteractionOverlay(false).emit(&parent); } } } diff --git a/src-tauri/src/services/presence_modules/models.rs b/src-tauri/src/services/presence_modules/models.rs index bdb4cc0..53d9d13 100644 --- a/src-tauri/src/services/presence_modules/models.rs +++ b/src-tauri/src/services/presence_modules/models.rs @@ -1,18 +1,16 @@ use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use specta::Type; -#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct PresenceStatus { pub title: Option, pub subtitle: Option, pub graphics_b64: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, Type)] #[serde(rename_all = "camelCase")] -#[ts(export)] pub struct ModuleMetadata { pub id: String, pub name: String, diff --git a/src-tauri/src/services/scene.rs b/src-tauri/src/services/scene.rs index 0060ef5..8047a08 100644 --- a/src-tauri/src/services/scene.rs +++ b/src-tauri/src/services/scene.rs @@ -4,11 +4,12 @@ use std::thread; use device_query::{DeviceQuery, DeviceState, Keycode}; use once_cell::sync::OnceCell; -use tauri::{Emitter, Manager}; +use tauri::Manager; use tauri_plugin_positioner::WindowExt; +use tauri_specta::Event as _; use tracing::{error, info, warn}; -use crate::{get_app_handle, services::app_events::AppEvents}; +use crate::{get_app_handle, services::app_events::SceneInteractiveChanged}; pub static SCENE_WINDOW_LABEL: &str = "scene"; pub static SPLASH_WINDOW_LABEL: &str = "splash"; @@ -69,7 +70,7 @@ pub fn update_scene_interactive(interactive: bool, should_click: bool) { } } - if let Err(e) = window.emit(AppEvents::SceneInteractive.as_str(), &interactive) { + if let Err(e) = SceneInteractiveChanged(interactive).emit(&window) { error!("Failed to emit scene interactive event: {}", e); } } else { @@ -78,16 +79,19 @@ pub fn update_scene_interactive(interactive: bool, should_click: bool) { } #[tauri::command] +#[specta::specta] pub fn set_scene_interactive(interactive: bool, should_click: bool) { update_scene_interactive(interactive, should_click); } #[tauri::command] +#[specta::specta] pub fn get_scene_interactive() -> Result { Ok(scene_interactive_state().load(Ordering::SeqCst)) } #[tauri::command] +#[specta::specta] pub fn set_pet_menu_state(id: String, open: bool) { let menus_arc = get_open_pet_menus(); let should_update = { diff --git a/src-tauri/src/services/ws/emitter.rs b/src-tauri/src/services/ws/emitter.rs index 484b4e3..d99eb1e 100644 --- a/src-tauri/src/services/ws/emitter.rs +++ b/src-tauri/src/services/ws/emitter.rs @@ -1,6 +1,7 @@ use rust_socketio::Payload; use serde::Serialize; -use tauri::{async_runtime, Emitter}; +use tauri::async_runtime; +use tauri_specta::Event; use tracing::{error, warn}; use crate::{ @@ -110,11 +111,8 @@ pub async fn ws_emit_soft( } } -/// Emit event to frontend (Tauri window) -/// -/// Handles error logging consistently. -pub fn emit_to_frontend(event: &str, payload: T) { - if let Err(e) = get_app_handle().emit(event, payload) { - error!("Failed to emit {} event to frontend: {:?}", event, e); +pub fn emit_to_frontend_typed(event: &E) { + if let Err(e) = event.emit(get_app_handle()) { + error!("Failed to emit {} event to frontend: {:?}", E::NAME, e); } } diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index b2e9753..03693db 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -6,7 +6,11 @@ use crate::models::event_payloads::{ FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendUserStatusPayload, UnfriendedPayload, }; -use crate::services::app_events::AppEvents; +use crate::services::app_events::{ + FriendActiveDollChanged, FriendCursorPositionUpdated, FriendDisconnected, + FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, + Unfriended, +}; use crate::services::cursor::{normalized_to_absolute, CursorPositions}; use crate::state::AppDataRefreshScope; @@ -21,7 +25,7 @@ pub fn on_friend_request_received(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-request-received") { - emitter::emit_to_frontend(AppEvents::FriendRequestReceived.as_str(), data); + emitter::emit_to_frontend_typed(&FriendRequestReceived(data)); } } @@ -30,7 +34,7 @@ pub fn on_friend_request_accepted(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-request-accepted") { - emitter::emit_to_frontend(AppEvents::FriendRequestAccepted.as_str(), data); + emitter::emit_to_frontend_typed(&FriendRequestAccepted(data)); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -40,14 +44,14 @@ pub fn on_friend_request_denied(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-request-denied") { - emitter::emit_to_frontend(AppEvents::FriendRequestDenied.as_str(), data); + emitter::emit_to_frontend_typed(&FriendRequestDenied(data)); } } /// Handler for unfriended event pub fn on_unfriended(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "unfriended") { - emitter::emit_to_frontend(AppEvents::Unfriended.as_str(), data); + emitter::emit_to_frontend_typed(&Unfriended(data)); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -68,7 +72,7 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { }, }; - emitter::emit_to_frontend(AppEvents::FriendCursorPosition.as_str(), outgoing_payload); + emitter::emit_to_frontend_typed(&FriendCursorPositionUpdated(outgoing_payload)); } } @@ -77,7 +81,7 @@ pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-disconnected") { - emitter::emit_to_frontend(AppEvents::FriendDisconnected.as_str(), data); + emitter::emit_to_frontend_typed(&FriendDisconnected(data)); } } @@ -110,7 +114,7 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { payload, "friend-active-doll-changed", ) { - emitter::emit_to_frontend(AppEvents::FriendActiveDollChanged.as_str(), data); + emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data)); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -120,6 +124,6 @@ pub fn on_friend_user_status(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-user-status") { - emitter::emit_to_frontend(AppEvents::FriendUserStatus.as_str(), data); + emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data)); } } diff --git a/src-tauri/src/services/ws/interaction.rs b/src-tauri/src/services/ws/interaction.rs index f2c8943..0f19dab 100644 --- a/src-tauri/src/services/ws/interaction.rs +++ b/src-tauri/src/services/ws/interaction.rs @@ -1,7 +1,7 @@ use rust_socketio::{Payload, RawClient}; use crate::models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}; -use crate::services::app_events::AppEvents; +use crate::services::app_events::{InteractionDeliveryFailed, InteractionReceived}; use super::{emitter, utils}; @@ -10,7 +10,7 @@ pub fn on_interaction_received(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "interaction-received") { - emitter::emit_to_frontend(AppEvents::InteractionReceived.as_str(), data); + emitter::emit_to_frontend_typed(&InteractionReceived(data)); } } @@ -20,6 +20,6 @@ pub fn on_interaction_delivery_failed(payload: Payload, _socket: RawClient) { payload, "interaction-delivery-failed", ) { - emitter::emit_to_frontend(AppEvents::InteractionDeliveryFailed.as_str(), data); + emitter::emit_to_frontend_typed(&InteractionDeliveryFailed(data)); } } diff --git a/src-tauri/src/services/ws/mod.rs b/src-tauri/src/services/ws/mod.rs index 7540195..a7906ba 100644 --- a/src-tauri/src/services/ws/mod.rs +++ b/src-tauri/src/services/ws/mod.rs @@ -29,4 +29,5 @@ pub mod client; // Re-export public API pub use cursor::report_cursor_data; pub use emitter::ws_emit_soft; +pub use types::OutgoingFriendCursorPayload; pub use types::WS_EVENT; diff --git a/src-tauri/src/services/ws/types.rs b/src-tauri/src/services/ws/types.rs index f2de5e5..f4d8e09 100644 --- a/src-tauri/src/services/ws/types.rs +++ b/src-tauri/src/services/ws/types.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use specta::Type; /// WebSocket event constants #[allow(non_camel_case_types)] @@ -37,7 +38,7 @@ pub struct IncomingFriendCursorPayload { } /// Outgoing friend cursor position to frontend -#[derive(Clone, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct OutgoingFriendCursorPayload { pub user_id: String, diff --git a/src-tauri/src/services/ws/user_status.rs b/src-tauri/src/services/ws/user_status.rs index a9abe66..32a6360 100644 --- a/src-tauri/src/services/ws/user_status.rs +++ b/src-tauri/src/services/ws/user_status.rs @@ -1,4 +1,5 @@ use once_cell::sync::Lazy; +use tauri_specta::Event as _; use tauri::async_runtime::{self, JoinHandle}; use tokio::sync::Mutex; use tokio::time::Duration; @@ -6,7 +7,7 @@ use tracing::warn; use crate::models::event_payloads::UserStatusPayload; -use crate::services::app_events::AppEvents; +use crate::services::app_events::UserStatusChanged; use super::{emitter, types::WS_EVENT}; @@ -23,7 +24,9 @@ pub async fn report_user_status(status: UserStatusPayload) { handle.abort(); } - emitter::emit_to_frontend(AppEvents::UserStatusChanged.as_str(), &status); + if let Err(e) = UserStatusChanged(status.clone()).emit(crate::get_app_handle()) { + warn!("Failed to emit user-status-changed event: {e}"); + } // Schedule new report after 500ms let handle = async_runtime::spawn(async move { diff --git a/src-tauri/src/state/ui.rs b/src-tauri/src/state/ui.rs index 6c240f7..bda3834 100644 --- a/src-tauri/src/state/ui.rs +++ b/src-tauri/src/state/ui.rs @@ -1,12 +1,12 @@ use crate::{ get_app_handle, lock_r, lock_w, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, - services::app_events::AppEvents, + services::app_events::AppDataRefreshed, state::FDOLL, }; use std::{collections::HashSet, sync::LazyLock}; -use tauri::Emitter; use tokio::sync::Mutex; +use tauri_specta::Event as _; use tracing::{info, warn}; pub fn update_display_dimensions_for_scene_state() { @@ -211,9 +211,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { let app_data_clone = guard.user_data.clone(); drop(guard); // Drop lock before emitting to prevent potential deadlocks - if let Err(e) = - get_app_handle().emit(AppEvents::AppDataRefreshed.as_str(), &app_data_clone) - { + if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) { warn!("Failed to emit app-data-refreshed event: {}", e); use tauri_plugin_dialog::MessageDialogBuilder; use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; diff --git a/src/events/app-data.ts b/src/events/app-data.ts index 3b2be92..e05d0a4 100644 --- a/src/events/app-data.ts +++ b/src/events/app-data.ts @@ -1,8 +1,5 @@ import { writable } from "svelte/store"; -import { type UserData } from "../types/bindings/UserData"; -import { listen } from "@tauri-apps/api/event"; -import { invoke } from "@tauri-apps/api/core"; -import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { commands, events, type UserData } from "$lib/bindings"; import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; export const appData = writable(null); @@ -16,13 +13,10 @@ const subscription = createListenerSubscription(); export async function startAppData() { try { if (subscription.isListening()) return; - appData.set(await invoke("get_app_data")); - const unlisten = await listen( - AppEvents.AppDataRefreshed, - (event) => { - appData.set(event.payload); - }, - ); + appData.set(await commands.getAppData()); + const unlisten = await events.appDataRefreshed.listen((event) => { + appData.set(event.payload); + }); subscription.setUnlisten(unlisten); subscription.setListening(true); } catch (error) { diff --git a/src/events/cursor.ts b/src/events/cursor.ts index ced6f4d..448eecb 100644 --- a/src/events/cursor.ts +++ b/src/events/cursor.ts @@ -1,8 +1,5 @@ -import { invoke } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; -import type { CursorPositions } from "../types/bindings/CursorPositions"; -import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { events, type CursorPositions } from "$lib/bindings"; import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; export const cursorPositionOnScreen = writable({ @@ -20,12 +17,9 @@ export async function startCursorTracking() { if (subscription.isListening()) return; try { - const unlisten = await listen( - AppEvents.CursorPosition, - (event) => { - cursorPositionOnScreen.set(event.payload); - }, - ); + const unlisten = await events.cursorMoved.listen((event) => { + cursorPositionOnScreen.set(event.payload); + }); subscription.setUnlisten(unlisten); subscription.setListening(true); } catch (err) { diff --git a/src/events/friend-cursor.ts b/src/events/friend-cursor.ts index 648bd8d..0f4fc29 100644 --- a/src/events/friend-cursor.ts +++ b/src/events/friend-cursor.ts @@ -1,21 +1,18 @@ -import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; -import type { CursorPositions } from "../types/bindings/CursorPositions"; -import type { DollDto } from "../types/bindings/DollDto"; -import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload"; -import type { FriendActiveDollChangedPayload } from "../types/bindings/FriendActiveDollChangedPayload"; -import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { + events, + type CursorPositions, + type DollDto, + type FriendActiveDollChangedPayload, + type FriendDisconnectedPayload, + type OutgoingFriendCursorPayload, +} from "$lib/bindings"; import { createMultiListenerSubscription, removeFromStore, setupHmrCleanup, } from "./listener-utils"; -export type FriendCursorPosition = { - userId: string; - position: CursorPositions; -}; - type FriendCursorData = { position: CursorPositions; lastUpdated: number; @@ -40,10 +37,9 @@ export async function startFriendCursorTracking() { try { // TODO: Add initial sync for existing friends' cursors and dolls if needed - const unlistenFriendCursor = await listen( - AppEvents.FriendCursorPosition, + const unlistenFriendCursor = await events.friendCursorPositionUpdated.listen( (event) => { - const data = event.payload; + const data: OutgoingFriendCursorPayload = event.payload; friendCursorState[data.userId] = { position: data.position, @@ -60,8 +56,7 @@ export async function startFriendCursorTracking() { ); subscription.addUnlisten(unlistenFriendCursor); - const unlistenFriendDisconnected = await listen( - AppEvents.FriendDisconnected, + const unlistenFriendDisconnected = await events.friendDisconnected.listen( (event) => { const data = event.payload; @@ -77,8 +72,7 @@ export async function startFriendCursorTracking() { subscription.addUnlisten(unlistenFriendDisconnected); const unlistenFriendActiveDollChanged = - await listen( - AppEvents.FriendActiveDollChanged, + await events.friendActiveDollChanged.listen( (event) => { const payload = event.payload; diff --git a/src/events/interaction.ts b/src/events/interaction.ts index 21904bc..0cabc7b 100644 --- a/src/events/interaction.ts +++ b/src/events/interaction.ts @@ -1,8 +1,9 @@ -import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; -import type { InteractionPayloadDto } from "../types/bindings/InteractionPayloadDto"; -import type { InteractionDeliveryFailedDto } from "../types/bindings/InteractionDeliveryFailedDto"; -import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { + events, + type InteractionDeliveryFailedDto, + type InteractionPayloadDto, +} from "$lib/bindings"; import { createMultiListenerSubscription, setupHmrCleanup, @@ -37,16 +38,12 @@ export async function startInteraction() { if (subscription.isListening()) return; try { - const unlistenReceived = await listen( - AppEvents.InteractionReceived, - (event) => { - addInteraction(event.payload); - }, - ); + const unlistenReceived = await events.interactionReceived.listen((event) => { + addInteraction(event.payload); + }); subscription.addUnlisten(unlistenReceived); - const unlistenFailed = await listen( - AppEvents.InteractionDeliveryFailed, + const unlistenFailed = await events.interactionDeliveryFailed.listen( (event) => { console.error("Interaction delivery failed:", event.payload); alert( diff --git a/src/events/scene-interactive.ts b/src/events/scene-interactive.ts index aaa5a15..ebfef49 100644 --- a/src/events/scene-interactive.ts +++ b/src/events/scene-interactive.ts @@ -1,7 +1,5 @@ -import { listen } from "@tauri-apps/api/event"; -import { invoke } from "@tauri-apps/api/core"; import { writable } from "svelte/store"; -import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { commands, events } from "$lib/bindings"; import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; export const sceneInteractive = writable(false); @@ -16,13 +14,10 @@ export async function startSceneInteractive() { if (subscription.isListening()) return; try { - sceneInteractive.set(await invoke("get_scene_interactive")); - const unlisten = await listen( - AppEvents.SceneInteractive, - (event) => { - sceneInteractive.set(Boolean(event.payload)); - }, - ); + sceneInteractive.set(await commands.getSceneInteractive()); + const unlisten = await events.sceneInteractiveChanged.listen((event) => { + sceneInteractive.set(Boolean(event.payload)); + }); subscription.setUnlisten(unlisten); subscription.setListening(true); } catch (error) { diff --git a/src/events/user-status.ts b/src/events/user-status.ts index d6757b3..852a4e8 100644 --- a/src/events/user-status.ts +++ b/src/events/user-status.ts @@ -1,9 +1,9 @@ -import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; -import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload"; -import type { FriendUserStatusPayload } from "../types/bindings/FriendUserStatusPayload"; -import type { UserStatusPayload } from "../types/bindings/UserStatusPayload"; -import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { + events, + type FriendDisconnectedPayload, + type UserStatusPayload, +} from "$lib/bindings"; import { createMultiListenerSubscription, removeFromStore, @@ -24,36 +24,31 @@ export async function startUserStatus() { if (subscription.isListening()) return; try { - const unlistenStatus = await listen( - AppEvents.FriendUserStatus, - (event) => { - const { userId, status } = event.payload; + const unlistenStatus = await events.friendUserStatusChanged.listen((event) => { + const { userId, status } = event.payload; - const hasValidName = - (typeof status.presenceStatus.title === "string" && - status.presenceStatus.title.trim() !== "") || - (typeof status.presenceStatus.subtitle === "string" && - status.presenceStatus.subtitle.trim() !== ""); - if (!hasValidName) return; + const hasValidName = + (typeof status.presenceStatus.title === "string" && + status.presenceStatus.title.trim() !== "") || + (typeof status.presenceStatus.subtitle === "string" && + status.presenceStatus.subtitle.trim() !== ""); + if (!hasValidName) return; - friendsPresenceStates.update((current) => ({ - ...current, - [userId]: status, - })); - }, - ); + friendsPresenceStates.update((current) => ({ + ...current, + [userId]: status, + })); + }); subscription.addUnlisten(unlistenStatus); - const unlistenUserStatusChanged = await listen( - AppEvents.UserStatusChanged, + const unlistenUserStatusChanged = await events.userStatusChanged.listen( (event) => { currentPresenceState.set(event.payload); }, ); subscription.addUnlisten(unlistenUserStatusChanged); - const unlistenFriendDisconnected = await listen( - AppEvents.FriendDisconnected, + const unlistenFriendDisconnected = await events.friendDisconnected.listen( (event) => { const { userId } = event.payload; friendsPresenceStates.update((current) => diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts new file mode 100644 index 0000000..e5f00cf --- /dev/null +++ b/src/lib/bindings.ts @@ -0,0 +1,281 @@ + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async getAppData() : Promise { + return await TAURI_INVOKE("get_app_data"); +}, +async refreshAppData() : Promise { + return await TAURI_INVOKE("refresh_app_data"); +}, +async listFriends() : Promise { + return await TAURI_INVOKE("list_friends"); +}, +async searchUsers(username: string | null) : Promise { + return await TAURI_INVOKE("search_users", { username }); +}, +async sendFriendRequest(request: SendFriendRequestDto) : Promise { + return await TAURI_INVOKE("send_friend_request", { request }); +}, +async receivedFriendRequests() : Promise { + return await TAURI_INVOKE("received_friend_requests"); +}, +async sentFriendRequests() : Promise { + return await TAURI_INVOKE("sent_friend_requests"); +}, +async acceptFriendRequest(requestId: string) : Promise { + return await TAURI_INVOKE("accept_friend_request", { requestId }); +}, +async denyFriendRequest(requestId: string) : Promise { + return await TAURI_INVOKE("deny_friend_request", { requestId }); +}, +async unfriend(friendId: string) : Promise { + return await TAURI_INVOKE("unfriend", { friendId }); +}, +async getDolls() : Promise { + return await TAURI_INVOKE("get_dolls"); +}, +async getDoll(id: string) : Promise { + return await TAURI_INVOKE("get_doll", { id }); +}, +async createDoll(dto: CreateDollDto) : Promise { + return await TAURI_INVOKE("create_doll", { dto }); +}, +async updateDoll(id: string, dto: UpdateDollDto) : Promise { + return await TAURI_INVOKE("update_doll", { id, dto }); +}, +async deleteDoll(id: string) : Promise { + return await TAURI_INVOKE("delete_doll", { id }); +}, +async setActiveDoll(dollId: string) : Promise { + return await TAURI_INVOKE("set_active_doll", { dollId }); +}, +async removeActiveDoll() : Promise { + return await TAURI_INVOKE("remove_active_doll"); +}, +async recolorGifBase64(whiteColorHex: string, blackColorHex: string, applyTexture: boolean) : Promise { + return await TAURI_INVOKE("recolor_gif_base64", { whiteColorHex, blackColorHex, applyTexture }); +}, +async encodePetDollGifBase64(doll: DollDto) : Promise { + return await TAURI_INVOKE("encode_pet_doll_gif_base64", { doll }); +}, +async quitApp() : Promise { + return await TAURI_INVOKE("quit_app"); +}, +async restartApp() : Promise { + await TAURI_INVOKE("restart_app"); +}, +/** + * Attempt to re-establish the user session without restarting the app. + * + * Validates server health, checks for a valid session token, + * then reconstructs the user session (re-fetches app data + WebSocket). + */ +async retryConnection() : Promise { + return await TAURI_INVOKE("retry_connection"); +}, +async getClientConfig() : Promise { + return await TAURI_INVOKE("get_client_config"); +}, +async saveClientConfig(config: AppConfig) : Promise { + return await TAURI_INVOKE("save_client_config", { config }); +}, +async openClientConfigManager() : Promise { + return await TAURI_INVOKE("open_client_config_manager"); +}, +async openDollEditorWindow(dollId: string | null) : Promise { + await TAURI_INVOKE("open_doll_editor_window", { dollId }); +}, +async getSceneInteractive() : Promise { + return await TAURI_INVOKE("get_scene_interactive"); +}, +async setSceneInteractive(interactive: boolean, shouldClick: boolean) : Promise { + await TAURI_INVOKE("set_scene_interactive", { interactive, shouldClick }); +}, +async setPetMenuState(id: string, open: boolean) : Promise { + await TAURI_INVOKE("set_pet_menu_state", { id, open }); +}, +async login(email: string, password: string) : Promise { + return await TAURI_INVOKE("login", { email, password }); +}, +async register(email: string, password: string, name: string | null, username: string | null) : Promise { + return await TAURI_INVOKE("register", { email, password, name, username }); +}, +async changePassword(currentPassword: string, newPassword: string) : Promise { + return await TAURI_INVOKE("change_password", { currentPassword, newPassword }); +}, +async resetPassword(oldPassword: string, newPassword: string) : Promise { + return await TAURI_INVOKE("reset_password", { oldPassword, newPassword }); +}, +async logoutAndRestart() : Promise { + return await TAURI_INVOKE("logout_and_restart"); +}, +async sendInteractionCmd(dto: SendInteractionDto) : Promise { + return await TAURI_INVOKE("send_interaction_cmd", { dto }); +}, +async getModules() : Promise { + return await TAURI_INVOKE("get_modules"); +} +} + +/** user-defined events **/ + + +export const events = __makeEvents__<{ +appDataRefreshed: AppDataRefreshed, +createDoll: CreateDoll, +cursorMoved: CursorMoved, +editDoll: EditDoll, +friendActiveDollChanged: FriendActiveDollChanged, +friendCursorPositionUpdated: FriendCursorPositionUpdated, +friendDisconnected: FriendDisconnected, +friendRequestAccepted: FriendRequestAccepted, +friendRequestDenied: FriendRequestDenied, +friendRequestReceived: FriendRequestReceived, +friendUserStatusChanged: FriendUserStatusChanged, +interactionDeliveryFailed: InteractionDeliveryFailed, +interactionReceived: InteractionReceived, +sceneInteractiveChanged: SceneInteractiveChanged, +setInteractionOverlay: SetInteractionOverlay, +unfriended: Unfriended, +userStatusChanged: UserStatusChanged +}>({ +appDataRefreshed: "app-data-refreshed", +createDoll: "create-doll", +cursorMoved: "cursor-moved", +editDoll: "edit-doll", +friendActiveDollChanged: "friend-active-doll-changed", +friendCursorPositionUpdated: "friend-cursor-position-updated", +friendDisconnected: "friend-disconnected", +friendRequestAccepted: "friend-request-accepted", +friendRequestDenied: "friend-request-denied", +friendRequestReceived: "friend-request-received", +friendUserStatusChanged: "friend-user-status-changed", +interactionDeliveryFailed: "interaction-delivery-failed", +interactionReceived: "interaction-received", +sceneInteractiveChanged: "scene-interactive-changed", +setInteractionOverlay: "set-interaction-overlay", +unfriended: "unfriended", +userStatusChanged: "user-status-changed" +}) + +/** user-defined constants **/ + + + +/** user-defined types **/ + +export type AppConfig = { api_base_url: string | null } +export type AppDataRefreshed = UserData +export type CreateDoll = null +export type CreateDollDto = { name: string; configuration: DollConfigurationDto | null } +export type CursorMoved = CursorPositions +export type CursorPosition = { x: number; y: number } +export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition } +export type DisplayData = { screen_width: number; screen_height: number; monitor_scale_factor: number } +export type DollColorSchemeDto = { outline: string; body: string } +export type DollConfigurationDto = { colorScheme: DollColorSchemeDto } +export type DollDto = { id: string; name: string; configuration: DollConfigurationDto; createdAt: string; updatedAt: string } +export type EditDoll = string +export type FriendActiveDollChanged = FriendActiveDollChangedPayload +export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null } +export type FriendCursorPositionUpdated = OutgoingFriendCursorPayload +export type FriendDisconnected = FriendDisconnectedPayload +export type FriendDisconnectedPayload = { userId: string } +export type FriendRequestAccepted = FriendRequestAcceptedPayload +export type FriendRequestAcceptedPayload = { id: string; friend: UserBasicDto; acceptedAt: string } +export type FriendRequestDenied = FriendRequestDeniedPayload +export type FriendRequestDeniedPayload = { id: string; denier: UserBasicDto; deniedAt: string } +export type FriendRequestReceived = FriendRequestReceivedPayload +export type FriendRequestReceivedPayload = { id: string; sender: UserBasicDto; createdAt: string } +export type FriendRequestResponseDto = { id: string; sender: UserBasicDto; receiver: UserBasicDto; status: string; createdAt: string; updatedAt: string } +export type FriendUserStatusChanged = FriendUserStatusPayload +export type FriendUserStatusPayload = { userId: string; status: UserStatusPayload } +export type FriendshipResponseDto = { id: string; friend: UserBasicDto | null; createdAt: string } +export type InteractionDeliveryFailed = InteractionDeliveryFailedDto +export type InteractionDeliveryFailedDto = { recipientUserId: string; reason: string } +export type InteractionPayloadDto = { senderUserId: string; senderName: string; content: string; type: string; timestamp: string } +export type InteractionReceived = InteractionPayloadDto +export type ModuleMetadata = { id: string; name: string; version: string; description: string | null } +/** + * Outgoing friend cursor position to frontend + */ +export type OutgoingFriendCursorPayload = { userId: string; position: CursorPositions } +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 SendFriendRequestDto = { receiverId: string } +export type SendInteractionDto = { recipientUserId: string; content: string; type: string } +export type SetInteractionOverlay = boolean +export type Unfriended = UnfriendedPayload +export type UnfriendedPayload = { friendId: string } +export type UpdateDollDto = { name: string | null; configuration: DollConfigurationDto | null } +export type UserBasicDto = { id: string; name: string; username: string | null; activeDoll: DollDto | null } +export type UserData = { user: UserProfile | null; friends: FriendshipResponseDto[] | null; dolls: DollDto[] | null; scene: SceneData } +export type UserProfile = { id: string; name: string; email: string; username: string | null; roles: string[]; createdAt: string; updatedAt: string; lastLoginAt: string | null; activeDollId: string | null } +export type UserStatusChanged = UserStatusPayload +export type UserStatusPayload = { presenceStatus: PresenceStatus; state: UserStatusState } +export type UserStatusState = "idle" | "resting" + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/src/lib/utils/sprite-utils.ts b/src/lib/utils/sprite-utils.ts index 21b055d..e2aff2e 100644 --- a/src/lib/utils/sprite-utils.ts +++ b/src/lib/utils/sprite-utils.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { commands } from "$lib/bindings"; import onekoGif from "../../assets/oneko/oneko.gif"; export interface RecolorOptions { @@ -15,11 +15,11 @@ export async function getSpriteSheetUrl( } try { - const result = await invoke("recolor_gif_base64", { - whiteColorHex: options.bodyColor, - blackColorHex: options.outlineColor, - applyTexture: options.applyTexture ?? true, - }); + const result = await commands.recolorGifBase64( + options.bodyColor, + options.outlineColor, + options.applyTexture ?? true, + ); return `data:image/gif;base64,${result}`; } catch (e) { console.error("Failed to recolor sprite:", e); diff --git a/src/routes/app-menu/+page.svelte b/src/routes/app-menu/+page.svelte index d5576c5..989f4af 100644 --- a/src/routes/app-menu/+page.svelte +++ b/src/routes/app-menu/+page.svelte @@ -3,14 +3,13 @@ import Preferences from "./tabs/preferences.svelte"; import Modules from "./tabs/modules.svelte"; import YourDolls from "./tabs/your-dolls/index.svelte"; - import { listen } from "@tauri-apps/api/event"; + import { events } from "$lib/bindings"; import { onMount } from "svelte"; - import { AppEvents } from "../../types/bindings/AppEventsConstants"; let showInteractionOverlay = false; onMount(() => { - const unlisten = listen(AppEvents.SetInteractionOverlay, (event) => { + const unlisten = events.setInteractionOverlay.listen((event) => { showInteractionOverlay = event.payload as boolean; }); diff --git a/src/routes/app-menu/tabs/friends.svelte b/src/routes/app-menu/tabs/friends.svelte index 6869fd8..f75ec52 100644 --- a/src/routes/app-menu/tabs/friends.svelte +++ b/src/routes/app-menu/tabs/friends.svelte @@ -1,12 +1,13 @@ @@ -16,10 +16,7 @@ class="absolute inset-0 z-10 size-full" aria-label="Deactive scene interactive" onmousedown={async () => { - await invoke("set_scene_interactive", { - interactive: false, - shouldClick: true, - }); + await commands.setSceneInteractive(false, true); }}> 
diff --git a/src/routes/scene/components/debug-bar.svelte b/src/routes/scene/components/debug-bar.svelte index b49fd75..9fc69f2 100644 --- a/src/routes/scene/components/debug-bar.svelte +++ b/src/routes/scene/components/debug-bar.svelte @@ -1,6 +1,5 @@