migrate from ts-rs to tauri-specta

This commit is contained in:
2026-03-07 18:36:51 +08:00
parent f65d837841
commit 4d7e97771a
86 changed files with 766 additions and 609 deletions

View File

@@ -6,56 +6,35 @@ Passive social app connecting peers through mouse cursor interactions in the for
Desktop client app for Friendolls. 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` - `timeout 30 pnpm tauri dev`
- **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 <test_name>`
- **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
### TypeScript/Svelte ### 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) - **Framework**: SvelteKit in SPA mode (SSR disabled for Tauri)
- **Error handling**: Standard try/catch with console.error logging - **Styling**: TailwindCSS + DaisyUI
- **Responsibility**: Minimal logic & data handling, should play as stateless dumb client - **Responsibility**: Minimal logic & data handling, should play as stateless dumb client, communicate with Rust local backend via Tauri events
### Rust ### Rust
- **Error handling**: `thiserror::Error` derive with descriptive error messages - **Error handling**: `thiserror::Error` derive with descriptive error messages
- **Logging**: `tracing` crate for structured logging (info/warn/error) - **Logging**: `tracing` crate for structured logging (info/warn/error)
- **Async**: `tokio` runtime with `async`/`await` - **Async**: `tokio` runtime with `async`/`await`
- **Serialization**: `serde` with `Serialize`/`Deserialize`
- **Naming**: snake_case for functions/variables, PascalCase for types/structs - **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 - **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 - **Security**: Use secure storage (keyring) for sensitive data
- **Imports**: Group by standard library, then external crates, then local modules
- **Responsibility**: Handles app state & data, business logic, controls UI via events. - **Responsibility**: Handles app state & data, business logic, controls UI via events.
## Note ## Note
Be sure to gather sufficient context from codebase before proceeding with changes. Observe patterns and follow trends. 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.

View File

@@ -2,16 +2,4 @@
This repository contins source for Friendolls desktop app. Will add more info when the app scales. 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_ 🙏 > _To the gods of programming, please grant me the perseverance to push through and get this app into production_ 🙏

120
src-tauri/Cargo.lock generated
View File

@@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
@@ -1227,6 +1233,8 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"specta",
"specta-typescript",
"strum", "strum",
"tauri", "tauri",
"tauri-build", "tauri-build",
@@ -1235,13 +1243,13 @@ dependencies = [
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-positioner", "tauri-plugin-positioner",
"tauri-plugin-process", "tauri-plugin-process",
"tauri-specta",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
"tracing-appender", "tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"ts-rs",
"url", "url",
"windows 0.58.0", "windows 0.58.0",
] ]
@@ -3035,6 +3043,12 @@ dependencies = [
"windows-link 0.2.1", "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]] [[package]]
name = "pathdiff" name = "pathdiff"
version = "0.2.3" version = "0.2.3"
@@ -4237,6 +4251,50 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@@ -4487,6 +4545,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serialize-to-javascript", "serialize-to-javascript",
"specta",
"swift-rs", "swift-rs",
"tauri-build", "tauri-build",
"tauri-macros", "tauri-macros",
@@ -4737,6 +4796,34 @@ dependencies = [
"wry", "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]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.8.0" version = "2.8.0"
@@ -4810,15 +4897,6 @@ dependencies = [
"utf-8", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@@ -5225,28 +5303,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 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]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.21.0" version = "0.21.0"

View File

@@ -24,7 +24,9 @@ tauri-plugin-positioner = "2"
tauri-plugin-process = "2" tauri-plugin-process = "2"
reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] } reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] }
tokio-util = "0.7" 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"] } strum = { version = "0.26", features = ["derive"] }
device_query = "4.0.1" device_query = "4.0.1"
dotenvy = "0.15.7" dotenvy = "0.15.7"

View File

@@ -4,6 +4,7 @@ use crate::services::auth::get_session_token;
use tracing::info; use tracing::info;
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn quit_app() -> Result<(), String> { pub fn quit_app() -> Result<(), String> {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
app_handle.exit(0); app_handle.exit(0);
@@ -11,6 +12,7 @@ pub fn quit_app() -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn restart_app() { pub fn restart_app() {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
app_handle.restart(); app_handle.restart();
@@ -21,6 +23,7 @@ pub fn restart_app() {
/// Validates server health, checks for a valid session token, /// Validates server health, checks for a valid session token,
/// then reconstructs the user session (re-fetches app data + WebSocket). /// then reconstructs the user session (re-fetches app data + WebSocket).
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn retry_connection() -> Result<(), String> { pub async fn retry_connection() -> Result<(), String> {
info!("Retrying connection..."); info!("Retrying connection...");

View File

@@ -6,12 +6,14 @@ use crate::{
}; };
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_app_data() -> Result<UserData, String> { pub fn get_app_data() -> Result<UserData, String> {
let guard = lock_r!(FDOLL); let guard = lock_r!(FDOLL);
Ok(guard.user_data.clone()) Ok(guard.user_data.clone())
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn refresh_app_data() -> Result<UserData, String> { pub async fn refresh_app_data() -> Result<UserData, String> {
init_app_data_scoped(AppDataRefreshScope::All).await; init_app_data_scoped(AppDataRefreshScope::All).await;
let guard = lock_r!(FDOLL); let guard = lock_r!(FDOLL);
@@ -19,6 +21,7 @@ pub async fn refresh_app_data() -> Result<UserData, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_modules() -> Result<Vec<ModuleMetadata>, String> { pub fn get_modules() -> Result<Vec<ModuleMetadata>, String> {
let guard = lock_r!(FDOLL); let guard = lock_r!(FDOLL);
Ok(guard.modules.metadatas.clone()) Ok(guard.modules.metadatas.clone())

View File

@@ -1,11 +1,13 @@
use crate::services::auth; use crate::services::auth;
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn logout_and_restart() -> Result<(), String> { pub async fn logout_and_restart() -> Result<(), String> {
auth::logout_and_restart().await.map_err(|e| e.to_string()) auth::logout_and_restart().await.map_err(|e| e.to_string())
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn login(email: String, password: String) -> Result<(), String> { pub async fn login(email: String, password: String) -> Result<(), String> {
auth::login_and_init_session(&email, &password) auth::login_and_init_session(&email, &password)
.await .await
@@ -13,6 +15,7 @@ pub async fn login(email: String, password: String) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn register( pub async fn register(
email: String, email: String,
password: String, password: String,
@@ -30,6 +33,7 @@ pub async fn register(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn change_password( pub async fn change_password(
current_password: String, current_password: String,
new_password: String, new_password: String,
@@ -40,6 +44,7 @@ pub async fn change_password(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn reset_password(old_password: String, new_password: String) -> Result<(), String> { pub async fn reset_password(old_password: String, new_password: String) -> Result<(), String> {
auth::reset_password(&old_password, &new_password) auth::reset_password(&old_password, &new_password)
.await .await

View File

@@ -7,6 +7,7 @@ use crate::{
}; };
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_client_config() -> AppConfig { pub fn get_client_config() -> AppConfig {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
guard.app_config = load_app_config(); guard.app_config = load_app_config();
@@ -14,6 +15,7 @@ pub fn get_client_config() -> AppConfig {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn save_client_config(config: AppConfig) -> Result<(), String> { pub fn save_client_config(config: AppConfig) -> Result<(), String> {
match save_app_config(config) { match save_app_config(config) {
Ok(saved) => { Ok(saved) => {
@@ -26,6 +28,7 @@ pub fn save_client_config(config: AppConfig) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn open_client_config_manager() -> Result<(), String> { pub async fn open_client_config_manager() -> Result<(), String> {
open_config_manager_window().map_err(|e| e.to_string()) open_config_manager_window().map_err(|e| e.to_string())
} }

View File

@@ -9,6 +9,7 @@ use crate::{
}; };
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_dolls() -> Result<Vec<DollDto>, String> { pub async fn get_dolls() -> Result<Vec<DollDto>, String> {
DollsRemote::new() DollsRemote::new()
.get_dolls() .get_dolls()
@@ -17,6 +18,7 @@ pub async fn get_dolls() -> Result<Vec<DollDto>, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_doll(id: String) -> Result<DollDto, String> { pub async fn get_doll(id: String) -> Result<DollDto, String> {
DollsRemote::new() DollsRemote::new()
.get_doll(&id) .get_doll(&id)
@@ -25,6 +27,7 @@ pub async fn get_doll(id: String) -> Result<DollDto, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn create_doll(dto: CreateDollDto) -> Result<DollDto, String> { pub async fn create_doll(dto: CreateDollDto) -> Result<DollDto, String> {
let result = DollsRemote::new() let result = DollsRemote::new()
.create_doll(dto) .create_doll(dto)
@@ -37,6 +40,7 @@ pub async fn create_doll(dto: CreateDollDto) -> Result<DollDto, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result<DollDto, String> { pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result<DollDto, String> {
let result = DollsRemote::new() let result = DollsRemote::new()
.update_doll(&id, dto) .update_doll(&id, dto)
@@ -55,6 +59,7 @@ pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result<DollDto, Stri
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn delete_doll(id: String) -> Result<(), String> { pub async fn delete_doll(id: String) -> Result<(), String> {
DollsRemote::new() DollsRemote::new()
.delete_doll(&id) .delete_doll(&id)
@@ -73,6 +78,7 @@ pub async fn delete_doll(id: String) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn set_active_doll(doll_id: String) -> Result<(), String> { pub async fn set_active_doll(doll_id: String) -> Result<(), String> {
UserRemote::new() UserRemote::new()
.set_active_doll(&doll_id) .set_active_doll(&doll_id)
@@ -85,6 +91,7 @@ pub async fn set_active_doll(doll_id: String) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn remove_active_doll() -> Result<(), String> { pub async fn remove_active_doll() -> Result<(), String> {
UserRemote::new() UserRemote::new()
.remove_active_doll() .remove_active_doll()

View File

@@ -6,6 +6,7 @@ use crate::state::AppDataRefreshScope;
use crate::commands::refresh_app_data; use crate::commands::refresh_app_data;
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn list_friends() -> Result<Vec<FriendshipResponseDto>, String> { pub async fn list_friends() -> Result<Vec<FriendshipResponseDto>, String> {
FriendRemote::new() FriendRemote::new()
.get_friends() .get_friends()
@@ -14,6 +15,7 @@ pub async fn list_friends() -> Result<Vec<FriendshipResponseDto>, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn search_users(username: Option<String>) -> Result<Vec<UserBasicDto>, String> { pub async fn search_users(username: Option<String>) -> Result<Vec<UserBasicDto>, String> {
tracing::info!( tracing::info!(
"Tauri command search_users called with username: {:?}", "Tauri command search_users called with username: {:?}",
@@ -30,6 +32,7 @@ pub async fn search_users(username: Option<String>) -> Result<Vec<UserBasicDto>,
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn send_friend_request( pub async fn send_friend_request(
request: SendFriendRequestDto, request: SendFriendRequestDto,
) -> Result<FriendRequestResponseDto, String> { ) -> Result<FriendRequestResponseDto, String> {
@@ -44,6 +47,7 @@ pub async fn send_friend_request(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn received_friend_requests() -> Result<Vec<FriendRequestResponseDto>, String> { pub async fn received_friend_requests() -> Result<Vec<FriendRequestResponseDto>, String> {
FriendRemote::new() FriendRemote::new()
.get_received_requests() .get_received_requests()
@@ -52,6 +56,7 @@ pub async fn received_friend_requests() -> Result<Vec<FriendRequestResponseDto>,
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn sent_friend_requests() -> Result<Vec<FriendRequestResponseDto>, String> { pub async fn sent_friend_requests() -> Result<Vec<FriendRequestResponseDto>, String> {
FriendRemote::new() FriendRemote::new()
.get_sent_requests() .get_sent_requests()
@@ -60,6 +65,7 @@ pub async fn sent_friend_requests() -> Result<Vec<FriendRequestResponseDto>, Str
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn accept_friend_request(request_id: String) -> Result<FriendRequestResponseDto, String> { pub async fn accept_friend_request(request_id: String) -> Result<FriendRequestResponseDto, String> {
let result = FriendRemote::new() let result = FriendRemote::new()
.accept_friend_request(&request_id) .accept_friend_request(&request_id)
@@ -72,6 +78,7 @@ pub async fn accept_friend_request(request_id: String) -> Result<FriendRequestRe
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn deny_friend_request(request_id: String) -> Result<FriendRequestResponseDto, String> { pub async fn deny_friend_request(request_id: String) -> Result<FriendRequestResponseDto, String> {
let result = FriendRemote::new() let result = FriendRemote::new()
.deny_friend_request(&request_id) .deny_friend_request(&request_id)
@@ -84,6 +91,7 @@ pub async fn deny_friend_request(request_id: String) -> Result<FriendRequestResp
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn unfriend(friend_id: String) -> Result<(), String> { pub async fn unfriend(friend_id: String) -> Result<(), String> {
FriendRemote::new() FriendRemote::new()
.unfriend(&friend_id) .unfriend(&friend_id)

View File

@@ -1,6 +1,7 @@
use crate::{models::interaction::SendInteractionDto, services::interaction::send_interaction}; use crate::{models::interaction::SendInteractionDto, services::interaction::send_interaction};
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn send_interaction_cmd(dto: SendInteractionDto) -> Result<(), String> { pub async fn send_interaction_cmd(dto: SendInteractionDto) -> Result<(), String> {
send_interaction(dto).await send_interaction(dto).await
} }

View File

@@ -2,6 +2,7 @@ use crate::models::dolls::DollDto;
use crate::services::petpet; use crate::services::petpet;
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result<String, String> { pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result<String, String> {
petpet::encode_pet_doll_gif_base64(doll) petpet::encode_pet_doll_gif_base64(doll)
} }

View File

@@ -1,6 +1,7 @@
use crate::services::sprite_recolor; use crate::services::sprite_recolor;
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn recolor_gif_base64( pub fn recolor_gif_base64(
white_color_hex: String, white_color_hex: String,
black_color_hex: String, black_color_hex: String,

View File

@@ -19,7 +19,17 @@ use commands::friends::{
use commands::interaction::send_interaction_cmd; use commands::interaction::send_interaction_cmd;
use commands::sprite::recolor_gif_base64; use commands::sprite::recolor_gif_base64;
use commands::petpet::encode_pet_doll_gif_base64; use commands::petpet::encode_pet_doll_gif_base64;
use specta_typescript::Typescript;
use tauri::async_runtime; 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<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new(); static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
@@ -51,12 +61,9 @@ fn register_app_events(event: tauri::RunEvent) {
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() let specta_builder = SpectaBuilder::<tauri::Wry>::new()
.plugin(tauri_plugin_opener::init()) .error_handling(ErrorHandlingMode::Throw)
.plugin(tauri_plugin_positioner::init()) .commands(collect_commands![
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
.invoke_handler(tauri::generate_handler![
get_app_data, get_app_data,
refresh_app_data, refresh_app_data,
list_friends, list_friends,
@@ -94,7 +101,39 @@ pub fn run() {
send_interaction_cmd, send_interaction_cmd,
get_modules 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 APP_HANDLE
.set(app.handle().to_owned()) .set(app.handle().to_owned())
.expect("Failed to init app handle."); .expect("Failed to init app handle.");

View File

@@ -1,10 +1,9 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use specta::Type;
use crate::models::{dolls::DollDto, friends::FriendshipResponseDto, user::UserProfile}; use crate::models::{dolls::DollDto, friends::FriendshipResponseDto, user::UserProfile};
#[derive(Serialize, Deserialize, Clone, Debug, TS)] #[derive(Serialize, Deserialize, Clone, Debug, Type)]
#[ts(export)]
pub struct DisplayData { pub struct DisplayData {
pub screen_width: i32, pub screen_width: i32,
pub screen_height: i32, pub screen_height: i32,
@@ -21,8 +20,7 @@ impl Default for DisplayData {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, TS)] #[derive(Serialize, Deserialize, Clone, Debug, Type)]
#[ts(export)]
pub struct SceneData { pub struct SceneData {
pub display: DisplayData, pub display: DisplayData,
pub grid_size: i32, pub grid_size: i32,
@@ -37,8 +35,7 @@ impl Default for SceneData {
} }
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[ts(export)]
pub struct UserData { pub struct UserData {
pub user: Option<UserProfile>, pub user: Option<UserProfile>,
pub friends: Option<Vec<FriendshipResponseDto>>, pub friends: Option<Vec<FriendshipResponseDto>>,

View File

@@ -1,40 +1,35 @@
use serde::{Deserialize, Serialize}; 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")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DollColorSchemeDto { pub struct DollColorSchemeDto {
pub outline: String, pub outline: String,
pub body: String, pub body: String,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DollConfigurationDto { pub struct DollConfigurationDto {
pub color_scheme: DollColorSchemeDto, pub color_scheme: DollColorSchemeDto,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CreateDollDto { pub struct CreateDollDto {
pub name: String, pub name: String,
pub configuration: Option<DollConfigurationDto>, pub configuration: Option<DollConfigurationDto>,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct UpdateDollDto { pub struct UpdateDollDto {
pub name: Option<String>, pub name: Option<String>,
pub configuration: Option<DollConfigurationDto>, pub configuration: Option<DollConfigurationDto>,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DollDto { pub struct DollDto {
pub id: String, pub id: String,
pub name: String, pub name: String,

View File

@@ -1,79 +1,70 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use specta::Type;
use super::dolls::DollDto; use super::dolls::DollDto;
use super::friends::UserBasicDto; use super::friends::UserBasicDto;
use crate::services::presence_modules::models::PresenceStatus; use crate::services::presence_modules::models::PresenceStatus;
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[ts(export)]
pub enum UserStatusState { pub enum UserStatusState {
Idle, Idle,
Resting, Resting,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct UserStatusPayload { pub struct UserStatusPayload {
pub presence_status: PresenceStatus, pub presence_status: PresenceStatus,
pub state: UserStatusState, pub state: UserStatusState,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendUserStatusPayload { pub struct FriendUserStatusPayload {
pub user_id: String, pub user_id: String,
pub status: UserStatusPayload, pub status: UserStatusPayload,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendDisconnectedPayload { pub struct FriendDisconnectedPayload {
pub user_id: String, pub user_id: String,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendActiveDollChangedPayload { pub struct FriendActiveDollChangedPayload {
pub friend_id: String, pub friend_id: String,
pub doll: Option<DollDto>, pub doll: Option<DollDto>,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendRequestReceivedPayload { pub struct FriendRequestReceivedPayload {
pub id: String, pub id: String,
pub sender: UserBasicDto, pub sender: UserBasicDto,
pub created_at: String, pub created_at: String,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendRequestAcceptedPayload { pub struct FriendRequestAcceptedPayload {
pub id: String, pub id: String,
pub friend: UserBasicDto, pub friend: UserBasicDto,
pub accepted_at: String, pub accepted_at: String,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendRequestDeniedPayload { pub struct FriendRequestDeniedPayload {
pub id: String, pub id: String,
pub denier: UserBasicDto, pub denier: UserBasicDto,
pub denied_at: String, pub denied_at: String,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct UnfriendedPayload { pub struct UnfriendedPayload {
pub friend_id: String, pub friend_id: String,
} }

View File

@@ -1,11 +1,10 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use specta::Type;
use super::dolls::DollDto; use super::dolls::DollDto;
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct UserBasicDto { pub struct UserBasicDto {
pub id: String, pub id: String,
pub name: String, pub name: String,
@@ -13,25 +12,22 @@ pub struct UserBasicDto {
pub active_doll: Option<DollDto>, pub active_doll: Option<DollDto>,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendshipResponseDto { pub struct FriendshipResponseDto {
pub id: String, pub id: String,
pub friend: Option<UserBasicDto>, pub friend: Option<UserBasicDto>,
pub created_at: String, pub created_at: String,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SendFriendRequestDto { pub struct SendFriendRequestDto {
pub receiver_id: String, pub receiver_id: String,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FriendRequestResponseDto { pub struct FriendRequestResponseDto {
pub id: String, pub id: String,
pub sender: UserBasicDto, pub sender: UserBasicDto,

View File

@@ -1,11 +1,10 @@
use reqwest::StatusCode; use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
use thiserror::Error; 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")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct HealthResponseDto { pub struct HealthResponseDto {
pub status: String, pub status: String,
pub version: String, pub version: String,

View File

@@ -1,8 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use specta::Type;
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SendInteractionDto { pub struct SendInteractionDto {
pub recipient_user_id: String, pub recipient_user_id: String,
@@ -11,8 +10,7 @@ pub struct SendInteractionDto {
pub type_: String, pub type_: String,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InteractionPayloadDto { pub struct InteractionPayloadDto {
pub sender_user_id: String, pub sender_user_id: String,
@@ -23,8 +21,7 @@ pub struct InteractionPayloadDto {
pub timestamp: String, pub timestamp: String,
} }
#[derive(Clone, Serialize, Deserialize, Debug, TS)] #[derive(Clone, Serialize, Deserialize, Debug, Type)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InteractionDeliveryFailedDto { pub struct InteractionDeliveryFailedDto {
pub recipient_user_id: String, pub recipient_user_id: String,

View File

@@ -1,9 +1,8 @@
use serde::{Deserialize, Serialize}; 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")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct UserProfile { pub struct UserProfile {
pub id: String, pub id: String,
pub name: String, pub name: String,

View File

@@ -1,105 +1,84 @@
use serde::Serialize; use serde::{Deserialize, Serialize};
#[allow(unused_imports)] use specta::Type;
use std::{fs, path::Path}; use tauri_specta::Event;
use strum::{AsRefStr, EnumIter};
use ts_rs::TS;
#[derive(Serialize, TS, EnumIter, AsRefStr)] use crate::{
#[serde(rename_all = "kebab-case")] models::{
#[ts(export)] app_data::UserData,
pub enum AppEvents { event_payloads::{
CursorPosition, FriendActiveDollChangedPayload, FriendDisconnectedPayload,
SceneInteractive, FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
AppDataRefreshed, FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload,
SetInteractionOverlay, },
EditDoll, interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
CreateDoll, },
UserStatusChanged, services::{cursor::CursorPositions, ws::OutgoingFriendCursorPayload},
FriendCursorPosition, };
FriendDisconnected,
FriendActiveDollChanged,
FriendUserStatus,
InteractionReceived,
InteractionDeliveryFailed,
FriendRequestReceived,
FriendRequestAccepted,
FriendRequestDenied,
Unfriended,
}
impl AppEvents { #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
pub fn as_str(&self) -> &'static str { #[tauri_specta(event_name = "cursor-position")]
match self { pub struct CursorMoved(pub CursorPositions);
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",
}
}
}
#[test] #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
fn export_bindings_appeventsconsts() { #[tauri_specta(event_name = "scene-interactive")]
use strum::IntoEnumIterator; pub struct SceneInteractiveChanged(pub bool);
let some_export_dir = std::env::var("TS_RS_EXPORT_DIR") #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
.ok() #[tauri_specta(event_name = "app-data-refreshed")]
.map(|s| Path::new(&s).to_owned()); pub struct AppDataRefreshed(pub UserData);
let Some(export_dir) = some_export_dir else { #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
eprintln!("TS_RS_EXPORT_DIR not set, skipping constants export"); #[tauri_specta(event_name = "set-interaction-overlay")]
return; pub struct SetInteractionOverlay(pub bool);
};
let to_kebab_case = |s: &str| -> String { #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
let mut result = String::new(); #[tauri_specta(event_name = "edit-doll")]
for (i, c) in s.chars().enumerate() { pub struct EditDoll(pub String);
if c.is_uppercase() {
if i > 0 {
result.push('-');
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
result
};
let mut lines = vec![ #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
r#"// Auto-generated constants - DO NOT EDIT"#.to_string(), #[tauri_specta(event_name = "create-doll")]
r#"// Generated from Rust AppEvents enum"#.to_string(), pub struct CreateDoll;
"".to_string(),
"export const AppEvents = {".to_string(),
];
for variant in AppEvents::iter() { #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
let name = variant.as_ref(); #[tauri_specta(event_name = "user-status-changed")]
let kebab = to_kebab_case(name); pub struct UserStatusChanged(pub UserStatusPayload);
lines.push(format!(" {}: \"{}\",", name, kebab));
}
lines.push("} as const;".to_string()); #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
lines.push("".to_string()); #[tauri_specta(event_name = "friend-cursor-position")]
lines.push("export type AppEvents = typeof AppEvents[keyof typeof AppEvents];".to_string()); 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"); #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
if let Err(e) = fs::write(&constants_path, constants_content) { #[tauri_specta(event_name = "friend-active-doll-changed")]
eprintln!("Failed to write {}: {}", constants_path.display(), e); 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);

View File

@@ -1,6 +1,7 @@
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::Manager; use tauri::Manager;
use thiserror::Error; use thiserror::Error;
use tracing::{error, warn}; use tracing::{error, warn};
@@ -8,7 +9,7 @@ use url::Url;
use crate::get_app_handle; use crate::get_app_handle;
#[derive(Default, Serialize, Deserialize, Clone, Debug)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
pub struct AppConfig { pub struct AppConfig {
pub api_base_url: Option<String>, pub api_base_url: Option<String>,
} }

View File

@@ -1,26 +1,29 @@
use device_query::{DeviceEvents, DeviceEventsHandler}; use device_query::{DeviceEvents, DeviceEventsHandler};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
use tauri::Emitter;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, error, info, warn}; 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")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CursorPosition { pub struct CursorPosition {
pub x: f64, pub x: f64,
pub y: f64, pub y: f64,
} }
#[derive(Debug, Clone, Serialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CursorPositions { pub struct CursorPositions {
pub raw: CursorPosition, pub raw: CursorPosition,
pub mapped: 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; crate::services::ws::report_cursor_data(mapped_for_ws).await;
// 2. Broadcast to local windows // 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); error!("Failed to emit cursor position event: {:?}", e);
} }
} }

View File

@@ -1,9 +1,13 @@
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use tauri::WebviewWindow; use tauri::WebviewWindow;
use tauri::{Emitter, Listener, Manager}; use tauri::{Listener, Manager};
use tauri_specta::Event as _;
use tracing::{error, info}; 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"; static APP_MENU_WINDOW_LABEL: &str = "app_menu";
@@ -51,6 +55,7 @@ fn set_window_interaction(window: &WebviewWindow, enable: bool) {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn open_doll_editor_window(doll_id: Option<String>) { pub async fn open_doll_editor_window(doll_id: Option<String>) {
let app_handle = get_app_handle().clone(); let app_handle = get_app_handle().clone();
@@ -76,17 +81,17 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
// Ensure overlay is active on parent (redundancy for safety) // Ensure overlay is active on parent (redundancy for safety)
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) { 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); error!("Failed to ensure interaction overlay on parent: {}", e);
} }
} }
// Emit event to update context // Emit event to update context
if let Some(id) = doll_id { 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); 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); error!("Failed to emit create-doll event: {}", e);
} }
@@ -136,7 +141,7 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
let app_handle_clone = get_app_handle().clone(); let app_handle_clone = get_app_handle().clone();
// Emit event to show overlay // 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); error!("Failed to emit set-interaction-overlay event: {}", e);
} }
@@ -169,7 +174,7 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
parent.unlisten(id); parent.unlisten(id);
} }
// Remove overlay if we failed // Remove overlay if we failed
let _ = parent.emit(AppEvents::SetInteractionOverlay.as_str(), false); let _ = SetInteractionOverlay(false).emit(&parent);
} }
return; return;
} }
@@ -204,9 +209,7 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
parent.unlisten(id); parent.unlisten(id);
} }
// Remove overlay // Remove overlay
if let Err(e) = parent if let Err(e) = SetInteractionOverlay(false).emit(&parent) {
.emit(AppEvents::SetInteractionOverlay.as_str(), false)
{
error!("Failed to remove interaction overlay: {}", e); error!("Failed to remove interaction overlay: {}", e);
} }
} }
@@ -232,7 +235,7 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
if let Some(id) = parent_focus_listener_id { if let Some(id) = parent_focus_listener_id {
parent.unlisten(id); parent.unlisten(id);
} }
let _ = parent.emit(AppEvents::SetInteractionOverlay.as_str(), false); let _ = SetInteractionOverlay(false).emit(&parent);
} }
} }
} }

View File

@@ -1,18 +1,16 @@
use serde::{Deserialize, Serialize}; 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")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct PresenceStatus { pub struct PresenceStatus {
pub title: Option<String>, pub title: Option<String>,
pub subtitle: Option<String>, pub subtitle: Option<String>,
pub graphics_b64: Option<String>, pub graphics_b64: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, TS)] #[derive(Serialize, Deserialize, Debug, Clone, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ModuleMetadata { pub struct ModuleMetadata {
pub id: String, pub id: String,
pub name: String, pub name: String,

View File

@@ -4,11 +4,12 @@ use std::thread;
use device_query::{DeviceQuery, DeviceState, Keycode}; use device_query::{DeviceQuery, DeviceState, Keycode};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use tauri::{Emitter, Manager}; use tauri::Manager;
use tauri_plugin_positioner::WindowExt; use tauri_plugin_positioner::WindowExt;
use tauri_specta::Event as _;
use tracing::{error, info, warn}; 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 SCENE_WINDOW_LABEL: &str = "scene";
pub static SPLASH_WINDOW_LABEL: &str = "splash"; 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); error!("Failed to emit scene interactive event: {}", e);
} }
} else { } else {
@@ -78,16 +79,19 @@ pub fn update_scene_interactive(interactive: bool, should_click: bool) {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_scene_interactive(interactive: bool, should_click: bool) { pub fn set_scene_interactive(interactive: bool, should_click: bool) {
update_scene_interactive(interactive, should_click); update_scene_interactive(interactive, should_click);
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_scene_interactive() -> Result<bool, String> { pub fn get_scene_interactive() -> Result<bool, String> {
Ok(scene_interactive_state().load(Ordering::SeqCst)) Ok(scene_interactive_state().load(Ordering::SeqCst))
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_pet_menu_state(id: String, open: bool) { pub fn set_pet_menu_state(id: String, open: bool) {
let menus_arc = get_open_pet_menus(); let menus_arc = get_open_pet_menus();
let should_update = { let should_update = {

View File

@@ -1,6 +1,7 @@
use rust_socketio::Payload; use rust_socketio::Payload;
use serde::Serialize; use serde::Serialize;
use tauri::{async_runtime, Emitter}; use tauri::async_runtime;
use tauri_specta::Event;
use tracing::{error, warn}; use tracing::{error, warn};
use crate::{ use crate::{
@@ -110,11 +111,8 @@ pub async fn ws_emit_soft<T: Serialize + Send + 'static>(
} }
} }
/// Emit event to frontend (Tauri window) pub fn emit_to_frontend_typed<E: Event + Serialize + Clone>(event: &E) {
/// if let Err(e) = event.emit(get_app_handle()) {
/// Handles error logging consistently. error!("Failed to emit {} event to frontend: {:?}", E::NAME, e);
pub fn emit_to_frontend<T: Serialize + Clone>(event: &str, payload: T) {
if let Err(e) = get_app_handle().emit(event, payload) {
error!("Failed to emit {} event to frontend: {:?}", event, e);
} }
} }

View File

@@ -6,7 +6,11 @@ use crate::models::event_payloads::{
FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendUserStatusPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendUserStatusPayload,
UnfriendedPayload, 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::services::cursor::{normalized_to_absolute, CursorPositions};
use crate::state::AppDataRefreshScope; use crate::state::AppDataRefreshScope;
@@ -21,7 +25,7 @@ pub fn on_friend_request_received(payload: Payload, _socket: RawClient) {
if let Ok(data) = if let Ok(data) =
utils::extract_and_parse::<FriendRequestReceivedPayload>(payload, "friend-request-received") utils::extract_and_parse::<FriendRequestReceivedPayload>(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) = if let Ok(data) =
utils::extract_and_parse::<FriendRequestAcceptedPayload>(payload, "friend-request-accepted") utils::extract_and_parse::<FriendRequestAcceptedPayload>(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); refresh::refresh_app_data(AppDataRefreshScope::Friends);
} }
} }
@@ -40,14 +44,14 @@ pub fn on_friend_request_denied(payload: Payload, _socket: RawClient) {
if let Ok(data) = if let Ok(data) =
utils::extract_and_parse::<FriendRequestDeniedPayload>(payload, "friend-request-denied") utils::extract_and_parse::<FriendRequestDeniedPayload>(payload, "friend-request-denied")
{ {
emitter::emit_to_frontend(AppEvents::FriendRequestDenied.as_str(), data); emitter::emit_to_frontend_typed(&FriendRequestDenied(data));
} }
} }
/// Handler for unfriended event /// Handler for unfriended event
pub fn on_unfriended(payload: Payload, _socket: RawClient) { pub fn on_unfriended(payload: Payload, _socket: RawClient) {
if let Ok(data) = utils::extract_and_parse::<UnfriendedPayload>(payload, "unfriended") { if let Ok(data) = utils::extract_and_parse::<UnfriendedPayload>(payload, "unfriended") {
emitter::emit_to_frontend(AppEvents::Unfriended.as_str(), data); emitter::emit_to_frontend_typed(&Unfriended(data));
refresh::refresh_app_data(AppDataRefreshScope::Friends); 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) = if let Ok(data) =
utils::extract_and_parse::<FriendDisconnectedPayload>(payload, "friend-disconnected") utils::extract_and_parse::<FriendDisconnectedPayload>(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, payload,
"friend-active-doll-changed", "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); refresh::refresh_app_data(AppDataRefreshScope::Friends);
} }
} }
@@ -120,6 +124,6 @@ pub fn on_friend_user_status(payload: Payload, _socket: RawClient) {
if let Ok(data) = if let Ok(data) =
utils::extract_and_parse::<FriendUserStatusPayload>(payload, "friend-user-status") utils::extract_and_parse::<FriendUserStatusPayload>(payload, "friend-user-status")
{ {
emitter::emit_to_frontend(AppEvents::FriendUserStatus.as_str(), data); emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data));
} }
} }

View File

@@ -1,7 +1,7 @@
use rust_socketio::{Payload, RawClient}; use rust_socketio::{Payload, RawClient};
use crate::models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}; use crate::models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto};
use crate::services::app_events::AppEvents; use crate::services::app_events::{InteractionDeliveryFailed, InteractionReceived};
use super::{emitter, utils}; use super::{emitter, utils};
@@ -10,7 +10,7 @@ pub fn on_interaction_received(payload: Payload, _socket: RawClient) {
if let Ok(data) = if let Ok(data) =
utils::extract_and_parse::<InteractionPayloadDto>(payload, "interaction-received") utils::extract_and_parse::<InteractionPayloadDto>(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, payload,
"interaction-delivery-failed", "interaction-delivery-failed",
) { ) {
emitter::emit_to_frontend(AppEvents::InteractionDeliveryFailed.as_str(), data); emitter::emit_to_frontend_typed(&InteractionDeliveryFailed(data));
} }
} }

View File

@@ -29,4 +29,5 @@ pub mod client;
// Re-export public API // Re-export public API
pub use cursor::report_cursor_data; pub use cursor::report_cursor_data;
pub use emitter::ws_emit_soft; pub use emitter::ws_emit_soft;
pub use types::OutgoingFriendCursorPayload;
pub use types::WS_EVENT; pub use types::WS_EVENT;

View File

@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
/// WebSocket event constants /// WebSocket event constants
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
@@ -37,7 +38,7 @@ pub struct IncomingFriendCursorPayload {
} }
/// Outgoing friend cursor position to frontend /// Outgoing friend cursor position to frontend
#[derive(Clone, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OutgoingFriendCursorPayload { pub struct OutgoingFriendCursorPayload {
pub user_id: String, pub user_id: String,

View File

@@ -1,4 +1,5 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tauri_specta::Event as _;
use tauri::async_runtime::{self, JoinHandle}; use tauri::async_runtime::{self, JoinHandle};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time::Duration; use tokio::time::Duration;
@@ -6,7 +7,7 @@ use tracing::warn;
use crate::models::event_payloads::UserStatusPayload; use crate::models::event_payloads::UserStatusPayload;
use crate::services::app_events::AppEvents; use crate::services::app_events::UserStatusChanged;
use super::{emitter, types::WS_EVENT}; use super::{emitter, types::WS_EVENT};
@@ -23,7 +24,9 @@ pub async fn report_user_status(status: UserStatusPayload) {
handle.abort(); 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 // Schedule new report after 500ms
let handle = async_runtime::spawn(async move { let handle = async_runtime::spawn(async move {

View File

@@ -1,12 +1,12 @@
use crate::{ use crate::{
get_app_handle, lock_r, lock_w, get_app_handle, lock_r, lock_w,
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
services::app_events::AppEvents, services::app_events::AppDataRefreshed,
state::FDOLL, state::FDOLL,
}; };
use std::{collections::HashSet, sync::LazyLock}; use std::{collections::HashSet, sync::LazyLock};
use tauri::Emitter;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tauri_specta::Event as _;
use tracing::{info, warn}; use tracing::{info, warn};
pub fn update_display_dimensions_for_scene_state() { 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(); let app_data_clone = guard.user_data.clone();
drop(guard); // Drop lock before emitting to prevent potential deadlocks drop(guard); // Drop lock before emitting to prevent potential deadlocks
if let Err(e) = if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) {
get_app_handle().emit(AppEvents::AppDataRefreshed.as_str(), &app_data_clone)
{
warn!("Failed to emit app-data-refreshed event: {}", e); warn!("Failed to emit app-data-refreshed event: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder; use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; use tauri_plugin_dialog::{DialogExt, MessageDialogKind};

View File

@@ -1,8 +1,5 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type UserData } from "../types/bindings/UserData"; import { commands, events, type UserData } from "$lib/bindings";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { AppEvents } from "../types/bindings/AppEventsConstants";
import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; import { createListenerSubscription, setupHmrCleanup } from "./listener-utils";
export const appData = writable<UserData | null>(null); export const appData = writable<UserData | null>(null);
@@ -16,13 +13,10 @@ const subscription = createListenerSubscription();
export async function startAppData() { export async function startAppData() {
try { try {
if (subscription.isListening()) return; if (subscription.isListening()) return;
appData.set(await invoke("get_app_data")); appData.set(await commands.getAppData());
const unlisten = await listen<UserData>( const unlisten = await events.appDataRefreshed.listen((event) => {
AppEvents.AppDataRefreshed,
(event) => {
appData.set(event.payload); appData.set(event.payload);
}, });
);
subscription.setUnlisten(unlisten); subscription.setUnlisten(unlisten);
subscription.setListening(true); subscription.setListening(true);
} catch (error) { } catch (error) {

View File

@@ -1,8 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { CursorPositions } from "../types/bindings/CursorPositions"; import { events, type CursorPositions } from "$lib/bindings";
import { AppEvents } from "../types/bindings/AppEventsConstants";
import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; import { createListenerSubscription, setupHmrCleanup } from "./listener-utils";
export const cursorPositionOnScreen = writable<CursorPositions>({ export const cursorPositionOnScreen = writable<CursorPositions>({
@@ -20,12 +17,9 @@ export async function startCursorTracking() {
if (subscription.isListening()) return; if (subscription.isListening()) return;
try { try {
const unlisten = await listen<CursorPositions>( const unlisten = await events.cursorMoved.listen((event) => {
AppEvents.CursorPosition,
(event) => {
cursorPositionOnScreen.set(event.payload); cursorPositionOnScreen.set(event.payload);
}, });
);
subscription.setUnlisten(unlisten); subscription.setUnlisten(unlisten);
subscription.setListening(true); subscription.setListening(true);
} catch (err) { } catch (err) {

View File

@@ -1,21 +1,18 @@
import { listen } from "@tauri-apps/api/event";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { CursorPositions } from "../types/bindings/CursorPositions"; import {
import type { DollDto } from "../types/bindings/DollDto"; events,
import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload"; type CursorPositions,
import type { FriendActiveDollChangedPayload } from "../types/bindings/FriendActiveDollChangedPayload"; type DollDto,
import { AppEvents } from "../types/bindings/AppEventsConstants"; type FriendActiveDollChangedPayload,
type FriendDisconnectedPayload,
type OutgoingFriendCursorPayload,
} from "$lib/bindings";
import { import {
createMultiListenerSubscription, createMultiListenerSubscription,
removeFromStore, removeFromStore,
setupHmrCleanup, setupHmrCleanup,
} from "./listener-utils"; } from "./listener-utils";
export type FriendCursorPosition = {
userId: string;
position: CursorPositions;
};
type FriendCursorData = { type FriendCursorData = {
position: CursorPositions; position: CursorPositions;
lastUpdated: number; lastUpdated: number;
@@ -40,10 +37,9 @@ export async function startFriendCursorTracking() {
try { try {
// TODO: Add initial sync for existing friends' cursors and dolls if needed // TODO: Add initial sync for existing friends' cursors and dolls if needed
const unlistenFriendCursor = await listen<FriendCursorPosition>( const unlistenFriendCursor = await events.friendCursorPositionUpdated.listen(
AppEvents.FriendCursorPosition,
(event) => { (event) => {
const data = event.payload; const data: OutgoingFriendCursorPayload = event.payload;
friendCursorState[data.userId] = { friendCursorState[data.userId] = {
position: data.position, position: data.position,
@@ -60,8 +56,7 @@ export async function startFriendCursorTracking() {
); );
subscription.addUnlisten(unlistenFriendCursor); subscription.addUnlisten(unlistenFriendCursor);
const unlistenFriendDisconnected = await listen<FriendDisconnectedPayload>( const unlistenFriendDisconnected = await events.friendDisconnected.listen(
AppEvents.FriendDisconnected,
(event) => { (event) => {
const data = event.payload; const data = event.payload;
@@ -77,8 +72,7 @@ export async function startFriendCursorTracking() {
subscription.addUnlisten(unlistenFriendDisconnected); subscription.addUnlisten(unlistenFriendDisconnected);
const unlistenFriendActiveDollChanged = const unlistenFriendActiveDollChanged =
await listen<FriendActiveDollChangedPayload>( await events.friendActiveDollChanged.listen(
AppEvents.FriendActiveDollChanged,
(event) => { (event) => {
const payload = event.payload; const payload = event.payload;

View File

@@ -1,8 +1,9 @@
import { listen } from "@tauri-apps/api/event";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { InteractionPayloadDto } from "../types/bindings/InteractionPayloadDto"; import {
import type { InteractionDeliveryFailedDto } from "../types/bindings/InteractionDeliveryFailedDto"; events,
import { AppEvents } from "../types/bindings/AppEventsConstants"; type InteractionDeliveryFailedDto,
type InteractionPayloadDto,
} from "$lib/bindings";
import { import {
createMultiListenerSubscription, createMultiListenerSubscription,
setupHmrCleanup, setupHmrCleanup,
@@ -37,16 +38,12 @@ export async function startInteraction() {
if (subscription.isListening()) return; if (subscription.isListening()) return;
try { try {
const unlistenReceived = await listen<InteractionPayloadDto>( const unlistenReceived = await events.interactionReceived.listen((event) => {
AppEvents.InteractionReceived,
(event) => {
addInteraction(event.payload); addInteraction(event.payload);
}, });
);
subscription.addUnlisten(unlistenReceived); subscription.addUnlisten(unlistenReceived);
const unlistenFailed = await listen<InteractionDeliveryFailedDto>( const unlistenFailed = await events.interactionDeliveryFailed.listen(
AppEvents.InteractionDeliveryFailed,
(event) => { (event) => {
console.error("Interaction delivery failed:", event.payload); console.error("Interaction delivery failed:", event.payload);
alert( alert(

View File

@@ -1,7 +1,5 @@
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { AppEvents } from "../types/bindings/AppEventsConstants"; import { commands, events } from "$lib/bindings";
import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; import { createListenerSubscription, setupHmrCleanup } from "./listener-utils";
export const sceneInteractive = writable<boolean>(false); export const sceneInteractive = writable<boolean>(false);
@@ -16,13 +14,10 @@ export async function startSceneInteractive() {
if (subscription.isListening()) return; if (subscription.isListening()) return;
try { try {
sceneInteractive.set(await invoke("get_scene_interactive")); sceneInteractive.set(await commands.getSceneInteractive());
const unlisten = await listen<boolean>( const unlisten = await events.sceneInteractiveChanged.listen((event) => {
AppEvents.SceneInteractive,
(event) => {
sceneInteractive.set(Boolean(event.payload)); sceneInteractive.set(Boolean(event.payload));
}, });
);
subscription.setUnlisten(unlisten); subscription.setUnlisten(unlisten);
subscription.setListening(true); subscription.setListening(true);
} catch (error) { } catch (error) {

View File

@@ -1,9 +1,9 @@
import { listen } from "@tauri-apps/api/event";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload"; import {
import type { FriendUserStatusPayload } from "../types/bindings/FriendUserStatusPayload"; events,
import type { UserStatusPayload } from "../types/bindings/UserStatusPayload"; type FriendDisconnectedPayload,
import { AppEvents } from "../types/bindings/AppEventsConstants"; type UserStatusPayload,
} from "$lib/bindings";
import { import {
createMultiListenerSubscription, createMultiListenerSubscription,
removeFromStore, removeFromStore,
@@ -24,9 +24,7 @@ export async function startUserStatus() {
if (subscription.isListening()) return; if (subscription.isListening()) return;
try { try {
const unlistenStatus = await listen<FriendUserStatusPayload>( const unlistenStatus = await events.friendUserStatusChanged.listen((event) => {
AppEvents.FriendUserStatus,
(event) => {
const { userId, status } = event.payload; const { userId, status } = event.payload;
const hasValidName = const hasValidName =
@@ -40,20 +38,17 @@ export async function startUserStatus() {
...current, ...current,
[userId]: status, [userId]: status,
})); }));
}, });
);
subscription.addUnlisten(unlistenStatus); subscription.addUnlisten(unlistenStatus);
const unlistenUserStatusChanged = await listen<UserStatusPayload>( const unlistenUserStatusChanged = await events.userStatusChanged.listen(
AppEvents.UserStatusChanged,
(event) => { (event) => {
currentPresenceState.set(event.payload); currentPresenceState.set(event.payload);
}, },
); );
subscription.addUnlisten(unlistenUserStatusChanged); subscription.addUnlisten(unlistenUserStatusChanged);
const unlistenFriendDisconnected = await listen<FriendDisconnectedPayload>( const unlistenFriendDisconnected = await events.friendDisconnected.listen(
AppEvents.FriendDisconnected,
(event) => { (event) => {
const { userId } = event.payload; const { userId } = event.payload;
friendsPresenceStates.update((current) => friendsPresenceStates.update((current) =>

281
src/lib/bindings.ts Normal file
View File

@@ -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<UserData> {
return await TAURI_INVOKE("get_app_data");
},
async refreshAppData() : Promise<UserData> {
return await TAURI_INVOKE("refresh_app_data");
},
async listFriends() : Promise<FriendshipResponseDto[]> {
return await TAURI_INVOKE("list_friends");
},
async searchUsers(username: string | null) : Promise<UserBasicDto[]> {
return await TAURI_INVOKE("search_users", { username });
},
async sendFriendRequest(request: SendFriendRequestDto) : Promise<FriendRequestResponseDto> {
return await TAURI_INVOKE("send_friend_request", { request });
},
async receivedFriendRequests() : Promise<FriendRequestResponseDto[]> {
return await TAURI_INVOKE("received_friend_requests");
},
async sentFriendRequests() : Promise<FriendRequestResponseDto[]> {
return await TAURI_INVOKE("sent_friend_requests");
},
async acceptFriendRequest(requestId: string) : Promise<FriendRequestResponseDto> {
return await TAURI_INVOKE("accept_friend_request", { requestId });
},
async denyFriendRequest(requestId: string) : Promise<FriendRequestResponseDto> {
return await TAURI_INVOKE("deny_friend_request", { requestId });
},
async unfriend(friendId: string) : Promise<null> {
return await TAURI_INVOKE("unfriend", { friendId });
},
async getDolls() : Promise<DollDto[]> {
return await TAURI_INVOKE("get_dolls");
},
async getDoll(id: string) : Promise<DollDto> {
return await TAURI_INVOKE("get_doll", { id });
},
async createDoll(dto: CreateDollDto) : Promise<DollDto> {
return await TAURI_INVOKE("create_doll", { dto });
},
async updateDoll(id: string, dto: UpdateDollDto) : Promise<DollDto> {
return await TAURI_INVOKE("update_doll", { id, dto });
},
async deleteDoll(id: string) : Promise<null> {
return await TAURI_INVOKE("delete_doll", { id });
},
async setActiveDoll(dollId: string) : Promise<null> {
return await TAURI_INVOKE("set_active_doll", { dollId });
},
async removeActiveDoll() : Promise<null> {
return await TAURI_INVOKE("remove_active_doll");
},
async recolorGifBase64(whiteColorHex: string, blackColorHex: string, applyTexture: boolean) : Promise<string> {
return await TAURI_INVOKE("recolor_gif_base64", { whiteColorHex, blackColorHex, applyTexture });
},
async encodePetDollGifBase64(doll: DollDto) : Promise<string> {
return await TAURI_INVOKE("encode_pet_doll_gif_base64", { doll });
},
async quitApp() : Promise<null> {
return await TAURI_INVOKE("quit_app");
},
async restartApp() : Promise<void> {
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<null> {
return await TAURI_INVOKE("retry_connection");
},
async getClientConfig() : Promise<AppConfig> {
return await TAURI_INVOKE("get_client_config");
},
async saveClientConfig(config: AppConfig) : Promise<null> {
return await TAURI_INVOKE("save_client_config", { config });
},
async openClientConfigManager() : Promise<null> {
return await TAURI_INVOKE("open_client_config_manager");
},
async openDollEditorWindow(dollId: string | null) : Promise<void> {
await TAURI_INVOKE("open_doll_editor_window", { dollId });
},
async getSceneInteractive() : Promise<boolean> {
return await TAURI_INVOKE("get_scene_interactive");
},
async setSceneInteractive(interactive: boolean, shouldClick: boolean) : Promise<void> {
await TAURI_INVOKE("set_scene_interactive", { interactive, shouldClick });
},
async setPetMenuState(id: string, open: boolean) : Promise<void> {
await TAURI_INVOKE("set_pet_menu_state", { id, open });
},
async login(email: string, password: string) : Promise<null> {
return await TAURI_INVOKE("login", { email, password });
},
async register(email: string, password: string, name: string | null, username: string | null) : Promise<string> {
return await TAURI_INVOKE("register", { email, password, name, username });
},
async changePassword(currentPassword: string, newPassword: string) : Promise<null> {
return await TAURI_INVOKE("change_password", { currentPassword, newPassword });
},
async resetPassword(oldPassword: string, newPassword: string) : Promise<null> {
return await TAURI_INVOKE("reset_password", { oldPassword, newPassword });
},
async logoutAndRestart() : Promise<null> {
return await TAURI_INVOKE("logout_and_restart");
},
async sendInteractionCmd(dto: SendInteractionDto) : Promise<null> {
return await TAURI_INVOKE("send_interaction_cmd", { dto });
},
async getModules() : Promise<ModuleMetadata[]> {
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__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
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__<any>) => {
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);
}
},
});
},
},
);
}

View File

@@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/core"; import { commands } from "$lib/bindings";
import onekoGif from "../../assets/oneko/oneko.gif"; import onekoGif from "../../assets/oneko/oneko.gif";
export interface RecolorOptions { export interface RecolorOptions {
@@ -15,11 +15,11 @@ export async function getSpriteSheetUrl(
} }
try { try {
const result = await invoke<string>("recolor_gif_base64", { const result = await commands.recolorGifBase64(
whiteColorHex: options.bodyColor, options.bodyColor,
blackColorHex: options.outlineColor, options.outlineColor,
applyTexture: options.applyTexture ?? true, options.applyTexture ?? true,
}); );
return `data:image/gif;base64,${result}`; return `data:image/gif;base64,${result}`;
} catch (e) { } catch (e) {
console.error("Failed to recolor sprite:", e); console.error("Failed to recolor sprite:", e);

View File

@@ -3,14 +3,13 @@
import Preferences from "./tabs/preferences.svelte"; import Preferences from "./tabs/preferences.svelte";
import Modules from "./tabs/modules.svelte"; import Modules from "./tabs/modules.svelte";
import YourDolls from "./tabs/your-dolls/index.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 { onMount } from "svelte";
import { AppEvents } from "../../types/bindings/AppEventsConstants";
let showInteractionOverlay = false; let showInteractionOverlay = false;
onMount(() => { onMount(() => {
const unlisten = listen(AppEvents.SetInteractionOverlay, (event) => { const unlisten = events.setInteractionOverlay.listen((event) => {
showInteractionOverlay = event.payload as boolean; showInteractionOverlay = event.payload as boolean;
}); });

View File

@@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { listen } from "@tauri-apps/api/event"; import {
import { invoke } from "@tauri-apps/api/core"; commands,
events,
type FriendRequestResponseDto,
type FriendshipResponseDto,
type UserBasicDto,
} from "$lib/bindings";
import { appData } from "../../../events/app-data"; import { appData } from "../../../events/app-data";
import { AppEvents } from "../../../types/bindings/AppEventsConstants";
import type { FriendRequestResponseDto } from "../../../types/bindings/FriendRequestResponseDto.js";
import type { FriendshipResponseDto } from "../../../types/bindings/FriendshipResponseDto.js";
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto.js";
let received: FriendRequestResponseDto[] = []; let received: FriendRequestResponseDto[] = [];
let sent: FriendRequestResponseDto[] = []; let sent: FriendRequestResponseDto[] = [];
@@ -51,27 +52,27 @@
refreshSent(); refreshSent();
unlisteners.push( unlisteners.push(
await listen(AppEvents.FriendRequestReceived, () => { await events.friendRequestReceived.listen(() => {
refreshReceived(); refreshReceived();
}), }),
); );
unlisteners.push( unlisteners.push(
await listen(AppEvents.FriendRequestAccepted, () => { await events.friendRequestAccepted.listen(() => {
refreshSent(); refreshSent();
invoke("refresh_app_data"); commands.refreshAppData();
}), }),
); );
unlisteners.push( unlisteners.push(
await listen(AppEvents.FriendRequestDenied, () => { await events.friendRequestDenied.listen(() => {
refreshSent(); refreshSent();
}), }),
); );
unlisteners.push( unlisteners.push(
await listen(AppEvents.Unfriended, () => { await events.unfriended.listen(() => {
invoke("refresh_app_data"); commands.refreshAppData();
}), }),
); );
}); });
@@ -83,7 +84,7 @@
async function refreshReceived() { async function refreshReceived() {
loading.received = true; loading.received = true;
try { try {
received = await invoke("received_friend_requests"); received = await commands.receivedFriendRequests();
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);
} finally { } finally {
@@ -94,7 +95,7 @@
async function refreshSent() { async function refreshSent() {
loading.sent = true; loading.sent = true;
try { try {
sent = await invoke("sent_friend_requests"); sent = await commands.sentFriendRequests();
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);
} finally { } finally {
@@ -105,8 +106,8 @@
async function handleAccept(id: string) { async function handleAccept(id: string) {
loading.action = true; loading.action = true;
try { try {
await invoke("accept_friend_request", { requestId: id }); await commands.acceptFriendRequest(id);
await Promise.all([refreshReceived(), invoke("refresh_app_data")]); await Promise.all([refreshReceived(), commands.refreshAppData()]);
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);
} finally { } finally {
@@ -117,7 +118,7 @@
async function handleDeny(id: string) { async function handleDeny(id: string) {
loading.action = true; loading.action = true;
try { try {
await invoke("deny_friend_request", { requestId: id }); await commands.denyFriendRequest(id);
await refreshReceived(); await refreshReceived();
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);
@@ -129,8 +130,8 @@
async function handleUnfriend(friendId: string) { async function handleUnfriend(friendId: string) {
loading.action = true; loading.action = true;
try { try {
await invoke("unfriend", { friendId }); await commands.unfriend(friendId);
await invoke("refresh_app_data"); await commands.refreshAppData();
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);
} finally { } finally {
@@ -157,9 +158,7 @@
error = null; error = null;
try { try {
const results = await invoke<UserBasicDto[]>("search_users", { const results = await commands.searchUsers(sanitizedTerm);
username: sanitizedTerm,
});
const match = results.find( const match = results.find(
(user) => user.username?.toLowerCase() === normalizedTerm, (user) => user.username?.toLowerCase() === normalizedTerm,
); );
@@ -181,9 +180,7 @@
async function handleSendRequest(receiverId: string) { async function handleSendRequest(receiverId: string) {
loading.action = true; loading.action = true;
try { try {
await invoke("send_friend_request", { await commands.sendFriendRequest({ receiverId });
request: { receiverId },
});
await refreshSent(); await refreshSent();
} catch (e) { } catch (e) {
const msg = (e as Error)?.message ?? String(e); const msg = (e as Error)?.message ?? String(e);

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core"; import { commands, type ModuleMetadata } from "$lib/bindings";
import type { ModuleMetadata } from "../../../types/bindings/ModuleMetadata";
let modules: ModuleMetadata[] = []; let modules: ModuleMetadata[] = [];
let loading = false; let loading = false;
@@ -10,7 +9,7 @@
onMount(async () => { onMount(async () => {
loading = true; loading = true;
try { try {
modules = await invoke("get_modules"); modules = await commands.getModules();
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);
} finally { } finally {

View File

@@ -1,5 +1,5 @@
<script> <script lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { commands } from "$lib/bindings";
import { appData } from "../../../events/app-data"; import { appData } from "../../../events/app-data";
import Power from "../../../assets/icons/power.svelte"; import Power from "../../../assets/icons/power.svelte";
@@ -17,7 +17,7 @@
if (signingOut) return; if (signingOut) return;
signingOut = true; signingOut = true;
try { try {
await invoke("logout_and_restart"); await commands.logoutAndRestart();
} catch (error) { } catch (error) {
console.error("Failed to sign out", error); console.error("Failed to sign out", error);
signingOut = false; signingOut = false;
@@ -26,7 +26,7 @@
const openClientConfigManager = async () => { const openClientConfigManager = async () => {
try { try {
await invoke("open_client_config_manager"); await commands.openClientConfigManager();
} catch (error) { } catch (error) {
console.error("Failed to open client config manager", error); console.error("Failed to open client config manager", error);
} }
@@ -49,10 +49,10 @@
isChangingPassword = true; isChangingPassword = true;
try { try {
await invoke("change_password", { await commands.changePassword(
currentPassword: passwordForm.currentPassword, passwordForm.currentPassword,
newPassword: passwordForm.newPassword, passwordForm.newPassword,
}); );
passwordSuccess = "Password updated"; passwordSuccess = "Password updated";
passwordForm.currentPassword = ""; passwordForm.currentPassword = "";
passwordForm.newPassword = ""; passwordForm.newPassword = "";
@@ -131,7 +131,7 @@
<button <button
class="btn btn-error btn-square btn-soft" class="btn btn-error btn-square btn-soft"
onclick={async () => { onclick={async () => {
await invoke("quit_app"); await commands.quitApp();
}} }}
> >
<div class="scale-50"> <div class="scale-50">

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { DollDto } from "../../../../types/bindings/DollDto"; import type { DollDto, UserProfile } from "$lib/bindings";
import type { UserProfile } from "../../../../types/bindings/UserProfile";
import DollPreview from "../../components/doll-preview.svelte"; import DollPreview from "../../components/doll-preview.svelte";
import PawPrint from "../../../../assets/icons/paw-print.svelte"; import PawPrint from "../../../../assets/icons/paw-print.svelte";
import Backpack from "../../../../assets/icons/backpack.svelte"; import Backpack from "../../../../assets/icons/backpack.svelte";

View File

@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { commands, type DollDto, type UserProfile } from "$lib/bindings";
import { appData } from "../../../../events/app-data"; import { appData } from "../../../../events/app-data";
import type { DollDto } from "../../../../types/bindings/DollDto";
import type { UserProfile } from "../../../../types/bindings/UserProfile";
import DollsList from "./dolls-list.svelte"; import DollsList from "./dolls-list.svelte";
let loading = false; let loading = false;
@@ -16,17 +14,17 @@
$: initialLoading = $appData === null; $: initialLoading = $appData === null;
async function openCreateModal() { async function openCreateModal() {
await invoke("open_doll_editor_window", { dollId: null }); await commands.openDollEditorWindow(null);
} }
async function openEditModal(doll: DollDto) { async function openEditModal(doll: DollDto) {
await invoke("open_doll_editor_window", { dollId: doll.id }); await commands.openDollEditorWindow(doll.id);
} }
async function handleSetActiveDoll(dollId: string) { async function handleSetActiveDoll(dollId: string) {
try { try {
loading = true; loading = true;
await invoke("set_active_doll", { dollId }); await commands.setActiveDoll(dollId);
// No manual refresh needed - backend will refresh and emit app-data-refreshed // No manual refresh needed - backend will refresh and emit app-data-refreshed
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);
@@ -38,7 +36,7 @@
async function handleRemoveActiveDoll() { async function handleRemoveActiveDoll() {
try { try {
loading = true; loading = true;
await invoke("remove_active_doll"); await commands.removeActiveDoll();
// No manual refresh needed - backend will refresh and emit app-data-refreshed // No manual refresh needed - backend will refresh and emit app-data-refreshed
} catch (e) { } catch (e) {
error = (e as Error)?.message ?? String(e); error = (e as Error)?.message ?? String(e);

View File

@@ -1,10 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core"; import { commands, type AppConfig } from "$lib/bindings";
type AppConfig = {
api_base_url?: string | null;
};
let form: AppConfig = { let form: AppConfig = {
api_base_url: "", api_base_url: "",
@@ -17,7 +13,7 @@
const loadConfig = async () => { const loadConfig = async () => {
try { try {
const config = (await invoke("get_client_config")) as AppConfig; const config = await commands.getClientConfig();
form = { form = {
api_base_url: config.api_base_url ?? "", api_base_url: config.api_base_url ?? "",
}; };
@@ -55,10 +51,8 @@
successMessage = ""; successMessage = "";
restartError = ""; restartError = "";
try { try {
await invoke("save_client_config", { await commands.saveClientConfig({
config: {
api_base_url: form.api_base_url?.trim() || null, api_base_url: form.api_base_url?.trim() || null,
},
}); });
successMessage = "Success. Restart to apply changes."; successMessage = "Success. Restart to apply changes.";
@@ -72,7 +66,7 @@
const restart = async () => { const restart = async () => {
restartError = ""; restartError = "";
try { try {
await invoke("restart_app"); await commands.restartApp();
} catch (err) { } catch (err) {
restartError = `Restart failed: ${err}`; restartError = `Restart failed: ${err}`;
} }

View File

@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core"; import {
commands,
type CreateDollDto,
type DollDto,
type UpdateDollDto,
} from "$lib/bindings";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import type { DollDto } from "../../types/bindings/DollDto";
import type { CreateDollDto } from "../../types/bindings/CreateDollDto";
import type { UpdateDollDto } from "../../types/bindings/UpdateDollDto";
import DollPreview from "../app-menu/components/doll-preview.svelte"; import DollPreview from "../app-menu/components/doll-preview.svelte";
let mode: "create" | "edit" = "create"; let mode: "create" | "edit" = "create";
@@ -35,7 +37,7 @@
async function fetchDoll(id: string) { async function fetchDoll(id: string) {
loading = true; loading = true;
try { try {
const doll: DollDto = await invoke("get_doll", { id }); const doll: DollDto = await commands.getDoll(id);
name = doll.name; name = doll.name;
bodyColor = doll.configuration.colorScheme.body; bodyColor = doll.configuration.colorScheme.body;
outlineColor = doll.configuration.colorScheme.outline; outlineColor = doll.configuration.colorScheme.outline;
@@ -62,7 +64,7 @@
}, },
}, },
}; };
await invoke("create_doll", { dto }); await commands.createDoll(dto);
} else if (dollId) { } else if (dollId) {
const dto: UpdateDollDto = { const dto: UpdateDollDto = {
name, name,
@@ -73,7 +75,7 @@
}, },
}, },
}; };
await invoke("update_doll", { id: dollId, dto }); await commands.updateDoll(dollId, dto);
} }
// Close window on success // Close window on success

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core"; import { commands } from "$lib/bindings";
import { page } from "$app/stores"; import { page } from "$app/stores";
let errorMessage = ""; let errorMessage = "";
@@ -15,7 +15,7 @@
isRetrying = true; isRetrying = true;
errorMessage = ""; errorMessage = "";
try { try {
await invoke("retry_connection"); await commands.retryConnection();
} catch (err) { } catch (err) {
errorMessage = `${err}`; errorMessage = `${err}`;
isRetrying = false; isRetrying = false;
@@ -55,7 +55,7 @@
class="btn btn-outline" class="btn btn-outline"
onclick={async () => { onclick={async () => {
try { try {
await invoke("open_client_config_manager"); await commands.openClientConfigManager();
} catch (err) { } catch (err) {
errorMessage = `Failed to open config manager: ${err}`; errorMessage = `Failed to open config manager: ${err}`;
} }

View File

@@ -7,7 +7,7 @@
friendsPresenceStates, friendsPresenceStates,
currentPresenceState, currentPresenceState,
} from "../../events/user-status"; } from "../../events/user-status";
import { invoke } from "@tauri-apps/api/core"; import { commands } from "$lib/bindings";
import DebugBar from "./components/debug-bar.svelte"; import DebugBar from "./components/debug-bar.svelte";
</script> </script>
@@ -16,10 +16,7 @@
class="absolute inset-0 z-10 size-full" class="absolute inset-0 z-10 size-full"
aria-label="Deactive scene interactive" aria-label="Deactive scene interactive"
onmousedown={async () => { onmousedown={async () => {
await invoke("set_scene_interactive", { await commands.setSceneInteractive(false, true);
interactive: false,
shouldClick: true,
});
}}>&nbsp;</button }}>&nbsp;</button
> >
<div id="debug-bar"> <div id="debug-bar">

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { PresenceStatus } from "../../../types/bindings/PresenceStatus"; import type { PresenceStatus, UserStatusPayload } from "$lib/bindings";
import type { UserStatusPayload } from "../../../types/bindings/UserStatusPayload";
interface Friend { interface Friend {
friend?: { friend?: {

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { commands } from "$lib/bindings";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import DollPreview from "../app-menu/components/doll-preview.svelte"; import DollPreview from "../app-menu/components/doll-preview.svelte";
import ExternalLink from "../../assets/icons/external-link.svelte"; import ExternalLink from "../../assets/icons/external-link.svelte";
@@ -31,22 +31,19 @@
errorMessage = ""; errorMessage = "";
try { try {
if (useRegister) { if (useRegister) {
await invoke("register", { await commands.register(
email: form.email.trim(), form.email.trim(),
password: form.password, form.password,
name: form.name.trim() || null, form.name.trim() || null,
username: form.username.trim() || null, form.username.trim() || null,
}); );
useRegister = false; useRegister = false;
resetRegisterFields(); resetRegisterFields();
form.password = ""; form.password = "";
return; return;
} }
await invoke("login", { await commands.login(form.email.trim(), form.password);
email: form.email.trim(),
password: form.password,
});
await getCurrentWebviewWindow().close(); await getCurrentWebviewWindow().close();
} catch (error) { } catch (error) {
console.error("Failed to authenticate", error); console.error("Failed to authenticate", error);
@@ -62,7 +59,7 @@
const openClientConfigManager = async () => { const openClientConfigManager = async () => {
try { try {
await invoke("open_client_config_manager"); await commands.openClientConfigManager();
} catch (error) { } catch (error) {
console.error("Failed to open client config manager", error); console.error("Failed to open client config manager", error);
} }

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AppEvents = "cursor-position" | "scene-interactive" | "app-data-refreshed" | "set-interaction-overlay" | "edit-doll" | "create-doll" | "user-status-changed" | "friend-cursor-position" | "friend-disconnected" | "friend-active-doll-changed" | "friend-user-status" | "interaction-received" | "interaction-delivery-failed" | "friend-request-received" | "friend-request-accepted" | "friend-request-denied" | "unfriended";

View File

@@ -1,24 +0,0 @@
// Auto-generated constants - DO NOT EDIT
// Generated from Rust AppEvents enum
export const AppEvents = {
CursorPosition: "cursor-position",
SceneInteractive: "scene-interactive",
AppDataRefreshed: "app-data-refreshed",
SetInteractionOverlay: "set-interaction-overlay",
EditDoll: "edit-doll",
CreateDoll: "create-doll",
UserStatusChanged: "user-status-changed",
FriendCursorPosition: "friend-cursor-position",
FriendDisconnected: "friend-disconnected",
FriendActiveDollChanged: "friend-active-doll-changed",
FriendUserStatus: "friend-user-status",
InteractionReceived: "interaction-received",
InteractionDeliveryFailed: "interaction-delivery-failed",
FriendRequestReceived: "friend-request-received",
FriendRequestAccepted: "friend-request-accepted",
FriendRequestDenied: "friend-request-denied",
Unfriended: "unfriended",
} as const;
export type AppEvents = typeof AppEvents[keyof typeof AppEvents];

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollConfigurationDto } from "./DollConfigurationDto";
export type CreateDollDto = { name: string, configuration: DollConfigurationDto | null, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CursorPosition = { x: number, y: number, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CursorPosition } from "./CursorPosition";
export type CursorPositions = { raw: CursorPosition, mapped: CursorPosition, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DisplayData = { screen_width: number, screen_height: number, monitor_scale_factor: number, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DollColorSchemeDto = { outline: string, body: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollColorSchemeDto } from "./DollColorSchemeDto";
export type DollConfigurationDto = { colorScheme: DollColorSchemeDto, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollConfigurationDto } from "./DollConfigurationDto";
export type DollDto = { id: string, name: string, configuration: DollConfigurationDto, createdAt: string, updatedAt: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollDto } from "./DollDto";
export type FriendActiveDollChangedPayload = { friendId: string, doll: DollDto | null, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FriendDisconnectedPayload = { userId: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendRequestAcceptedPayload = { id: string, friend: UserBasicDto, acceptedAt: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendRequestDeniedPayload = { id: string, denier: UserBasicDto, deniedAt: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendRequestReceivedPayload = { id: string, sender: UserBasicDto, createdAt: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendRequestResponseDto = { id: string, sender: UserBasicDto, receiver: UserBasicDto, status: string, createdAt: string, updatedAt: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserStatusPayload } from "./UserStatusPayload";
export type FriendUserStatusPayload = { userId: string, status: UserStatusPayload, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendshipResponseDto = { id: string, friend: UserBasicDto | null, createdAt: string, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HealthResponseDto = { status: string, version: string, uptimeSecs: bigint, db: string, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InteractionDeliveryFailedDto = { recipientUserId: string, reason: string, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InteractionPayloadDto = { senderUserId: string, senderName: string, content: string, type: string, timestamp: string, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModuleMetadata = { id: string, name: string, version: string, description: string | null, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PresenceStatus = { title: string | null, subtitle: string | null, graphicsB64: string | null, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DisplayData } from "./DisplayData";
export type SceneData = { display: DisplayData, grid_size: number, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SendFriendRequestDto = { receiverId: string, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SendInteractionDto = { recipientUserId: string, content: string, type: string, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UnfriendedPayload = { friendId: string, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollConfigurationDto } from "./DollConfigurationDto";
export type UpdateDollDto = { name: string | null, configuration: DollConfigurationDto | null, };

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollDto } from "./DollDto";
export type UserBasicDto = { id: string, name: string, username: string | null, activeDoll: DollDto | null, };

View File

@@ -1,7 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollDto } from "./DollDto";
import type { FriendshipResponseDto } from "./FriendshipResponseDto";
import type { SceneData } from "./SceneData";
import type { UserProfile } from "./UserProfile";
export type UserData = { user: UserProfile | null, friends: Array<FriendshipResponseDto> | null, dolls: Array<DollDto> | null, scene: SceneData, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UserProfile = { id: string, name: string, email: string, username: string | null, roles: Array<string>, createdAt: string, updatedAt: string, lastLoginAt: string | null, activeDollId: string | null, };

View File

@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PresenceStatus } from "./PresenceStatus";
import type { UserStatusState } from "./UserStatusState";
export type UserStatusPayload = { presenceStatus: PresenceStatus, state: UserStatusState, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UserStatusState = "idle" | "resting";