trivial enhancements to state management solution

This commit is contained in:
2025-12-24 16:25:48 +08:00
parent 83f145f490
commit 42f798c8b7
11 changed files with 400 additions and 178 deletions

View File

@@ -6,12 +6,11 @@ use crate::{
UserBasicDto,
},
remotes::user::UserRemote,
services::cursor::start_cursor_tracking,
services::doll_editor::open_doll_editor_window,
state::{init_app_data, FDOLL},
services::{cursor::start_cursor_tracking, doll_editor::open_doll_editor_window},
state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL},
};
use tauri::async_runtime;
use tauri::{Emitter, Manager};
use tauri::Manager;
use tracing_subscriber::{self, util::SubscriberInitExt};
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
@@ -204,12 +203,11 @@ async fn create_doll(dto: CreateDollDto) -> Result<DollDto, String> {
.await
.map_err(|e| e.to_string())?;
// Emit event locally so the app knows about the update immediately
// The websocket event will also come in, but this makes the UI snappy
if let Err(e) = get_app_handle().emit("doll_created", &result) {
tracing::error!("Failed to emit local doll.created event: {}", e);
}
// Refresh dolls list in background (deduped inside init_app_data_scoped)
async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
});
Ok(result)
}
@@ -220,10 +218,26 @@ async fn update_doll(id: String, dto: UpdateDollDto) -> Result<DollDto, String>
.await
.map_err(|e| e.to_string())?;
// Emit event locally
if let Err(e) = get_app_handle().emit("doll_updated", &result) {
tracing::error!("Failed to emit local doll.updated event: {}", e);
}
// Check if this was the active doll (after update completes to avoid stale reads)
let is_active_doll = {
let guard = lock_r!(FDOLL);
guard
.app_data
.user
.as_ref()
.and_then(|u| u.active_doll_id.as_ref())
.map(|active_id| active_id == &id)
.unwrap_or(false)
};
// Refresh dolls list + User/Friends if this was the active doll
async_runtime::spawn(async move {
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
if is_active_doll {
init_app_data_scoped(AppDataRefreshScope::User).await;
init_app_data_scoped(AppDataRefreshScope::Friends).await;
}
});
Ok(result)
}
@@ -235,20 +249,26 @@ async fn delete_doll(id: String) -> Result<(), String> {
.await
.map_err(|e| e.to_string())?;
// Emit event locally
// We need to send something that matches the structure expected by the frontend
// The WS event sends the full object, but for deletion usually ID is enough or the object itself
// Let's send a dummy object with just the ID if we can, or just the ID if the frontend handles it.
// Looking at WS code: on_doll_deleted emits the payload it gets.
// Ideally we'd return the deleted doll from the backend but delete usually returns empty.
// Let's just emit the ID wrapped in an object or similar if needed.
// Actually, let's check what the frontend expects.
// src/routes/app-menu/tabs/your-dolls/index.svelte just listens and calls refreshDolls(),
// it doesn't use the payload. So any payload is fine.
if let Err(e) = get_app_handle().emit("doll_deleted", ()) {
tracing::error!("Failed to emit local doll.deleted event: {}", e);
}
// Check if this was the active doll (after delete completes to avoid stale reads)
let is_active_doll = {
let guard = lock_r!(FDOLL);
guard
.app_data
.user
.as_ref()
.and_then(|u| u.active_doll_id.as_ref())
.map(|active_id| active_id == &id)
.unwrap_or(false)
};
// Refresh dolls list + User/Friends if the deleted doll was active
async_runtime::spawn(async move {
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
if is_active_doll {
init_app_data_scoped(AppDataRefreshScope::User).await;
init_app_data_scoped(AppDataRefreshScope::Friends).await;
}
});
Ok(())
}
@@ -258,7 +278,16 @@ async fn set_active_doll(doll_id: String) -> Result<(), String> {
UserRemote::new()
.set_active_doll(&doll_id)
.await
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;
// Refresh User (for active_doll_id) + Friends (so friends see your active doll)
// We don't need to refresh Dolls since the doll itself hasn't changed
async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::User).await;
init_app_data_scoped(AppDataRefreshScope::Friends).await;
});
Ok(())
}
#[tauri::command]
@@ -266,7 +295,15 @@ async fn remove_active_doll() -> Result<(), String> {
UserRemote::new()
.remove_active_doll()
.await
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;
// Refresh User (for active_doll_id) + Friends (so friends see your doll is gone)
async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::User).await;
init_app_data_scoped(AppDataRefreshScope::Friends).await;
});
Ok(())
}
#[tauri::command]

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::remotes::{friends::FriendshipResponseDto, user::UserProfile};
use crate::remotes::{dolls::DollDto, friends::FriendshipResponseDto, user::UserProfile};
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
@@ -42,5 +42,6 @@ impl Default for SceneData {
pub struct AppData {
pub user: Option<UserProfile>,
pub friends: Option<Vec<FriendshipResponseDto>>,
pub dolls: Option<Vec<DollDto>>,
pub scene: SceneData,
}

View File

@@ -83,32 +83,44 @@ impl DollsRemote {
pub async fn get_dolls(&self) -> Result<Vec<DollDto>, RemoteError> {
let url = format!("{}/dolls/me", self.base_url);
tracing::info!("DollsRemote::get_dolls - Sending GET request to URL: {}", url);
tracing::info!(
"DollsRemote::get_dolls - Sending GET request to URL: {}",
url
);
let resp = with_auth(self.client.get(url)).await.send().await?;
let resp = resp.error_for_status().map_err(|e| {
tracing::error!("DollsRemote::get_dolls - HTTP error: {}", e);
e
})?;
let text = resp.text().await.map_err(|e| {
tracing::error!("DollsRemote::get_dolls - Failed to read response text: {}", e);
tracing::error!(
"DollsRemote::get_dolls - Failed to read response text: {}",
e
);
e
})?;
let dolls: Vec<DollDto> = serde_json::from_str(&text).map_err(|e| {
tracing::error!("DollsRemote::get_dolls - Failed to parse JSON: {}", e);
e
})?;
tracing::info!("DollsRemote::get_dolls - Successfully parsed {} dolls", dolls.len());
tracing::info!(
"DollsRemote::get_dolls - Successfully parsed {} dolls",
dolls.len()
);
Ok(dolls)
}
pub async fn get_doll(&self, id: &str) -> Result<DollDto, RemoteError> {
let url = format!("{}/dolls/{}", self.base_url, id);
tracing::info!("DollsRemote::get_doll - Sending GET request to URL: {}", url);
tracing::info!(
"DollsRemote::get_doll - Sending GET request to URL: {}",
url
);
let resp = with_auth(self.client.get(url)).await.send().await?;
let resp = resp.error_for_status().map_err(|e| {
@@ -117,7 +129,10 @@ impl DollsRemote {
})?;
let text = resp.text().await.map_err(|e| {
tracing::error!("DollsRemote::get_doll - Failed to read response text: {}", e);
tracing::error!(
"DollsRemote::get_doll - Failed to read response text: {}",
e
);
e
})?;
@@ -125,14 +140,17 @@ impl DollsRemote {
tracing::error!("DollsRemote::get_doll - Failed to parse JSON: {}", e);
e
})?;
Ok(doll)
}
pub async fn create_doll(&self, dto: CreateDollDto) -> Result<DollDto, RemoteError> {
let url = format!("{}/dolls", self.base_url);
tracing::info!("DollsRemote::create_doll - Sending POST request to URL: {}", url);
tracing::info!(
"DollsRemote::create_doll - Sending POST request to URL: {}",
url
);
let resp = with_auth(self.client.post(url))
.await
.json(&dto)
@@ -145,7 +163,10 @@ impl DollsRemote {
})?;
let text = resp.text().await.map_err(|e| {
tracing::error!("DollsRemote::create_doll - Failed to read response text: {}", e);
tracing::error!(
"DollsRemote::create_doll - Failed to read response text: {}",
e
);
e
})?;
@@ -153,14 +174,17 @@ impl DollsRemote {
tracing::error!("DollsRemote::create_doll - Failed to parse JSON: {}", e);
e
})?;
Ok(doll)
}
pub async fn update_doll(&self, id: &str, dto: UpdateDollDto) -> Result<DollDto, RemoteError> {
let url = format!("{}/dolls/{}", self.base_url, id);
tracing::info!("DollsRemote::update_doll - Sending PATCH request to URL: {}", url);
tracing::info!(
"DollsRemote::update_doll - Sending PATCH request to URL: {}",
url
);
let resp = with_auth(self.client.patch(url))
.await
.json(&dto)
@@ -173,7 +197,10 @@ impl DollsRemote {
})?;
let text = resp.text().await.map_err(|e| {
tracing::error!("DollsRemote::update_doll - Failed to read response text: {}", e);
tracing::error!(
"DollsRemote::update_doll - Failed to read response text: {}",
e
);
e
})?;
@@ -181,16 +208,19 @@ impl DollsRemote {
tracing::error!("DollsRemote::update_doll - Failed to parse JSON: {}", e);
e
})?;
Ok(doll)
}
pub async fn delete_doll(&self, id: &str) -> Result<(), RemoteError> {
let url = format!("{}/dolls/{}", self.base_url, id);
tracing::info!("DollsRemote::delete_doll - Sending DELETE request to URL: {}", url);
tracing::info!(
"DollsRemote::delete_doll - Sending DELETE request to URL: {}",
url
);
let resp = with_auth(self.client.delete(url)).await.send().await?;
resp.error_for_status().map_err(|e| {
tracing::error!("DollsRemote::delete_doll - HTTP error: {}", e);
e

View File

@@ -1,3 +1,3 @@
pub mod dolls;
pub mod friends;
pub mod user;
pub mod dolls;

View File

@@ -633,6 +633,15 @@ where
if let Err(e) = save_auth_pass(&auth_pass) {
error!("Failed to save auth pass: {}", e);
}
// Immediately refresh app data now that auth is available
tauri::async_runtime::spawn(async {
crate::state::init_app_data_scoped(
crate::state::AppDataRefreshScope::All,
)
.await;
});
on_success();
}
Err(e) => error!("Token exchange failed: {}", e),

View File

@@ -135,11 +135,8 @@ async fn init_cursor_tracking() -> Result<(), String> {
};
let mapped = absolute_to_normalized(&raw);
let positions = CursorPositions {
raw,
mapped,
};
let positions = CursorPositions { raw, mapped };
// Send to consumer channel (non-blocking)
if let Err(e) = tx.try_send(positions) {

View File

@@ -122,7 +122,7 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
// macOS Specific: Focus Trap Listener ID
// We need to capture this to unlisten later.
let mut parent_focus_listener_id: Option<u32> = None;
if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) {

View File

@@ -7,7 +7,7 @@ use crate::{
get_app_handle, lock_r, lock_w,
models::app_config::AppConfig,
services::cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
state::FDOLL,
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
};
use serde::{Deserialize, Serialize};
@@ -59,7 +59,7 @@ fn on_initialized(payload: Payload, _socket: RawClient) {
Payload::Text(values) => {
if let Some(first_value) = values.first() {
info!("Received initialized event: {:?}", first_value);
// Mark WebSocket as initialized
let mut guard = lock_w!(FDOLL);
if let Some(clients) = guard.clients.as_mut() {
@@ -93,6 +93,11 @@ fn on_friend_request_accepted(payload: Payload, _socket: RawClient) {
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str) {
error!("Failed to emit friend request accepted event: {:?}", e);
}
// Refresh friends list only (optimized - no need to fetch user profile)
tauri::async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::Friends).await;
});
}
_ => error!("Received unexpected payload format for friend request accepted"),
}
@@ -117,6 +122,11 @@ fn on_unfriended(payload: Payload, _socket: RawClient) {
if let Err(e) = get_app_handle().emit(WS_EVENT::UNFRIENDED, str) {
error!("Failed to emit unfriended event: {:?}", e);
}
// Refresh friends list only (optimized - no need to fetch user profile)
tauri::async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::Friends).await;
});
}
_ => error!("Received unexpected payload format for unfriended"),
}
@@ -232,6 +242,12 @@ fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
} else {
info!("Emitted friend-active-doll-changed to frontend");
}
// Refresh friends list only (optimized - friend's active doll is part of friends data)
// Deduplicate burst events inside init_app_data_scoped.
tauri::async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::Friends).await;
});
} else {
info!("Received friend-active-doll-changed event with empty payload");
}
@@ -245,9 +261,11 @@ fn on_doll_created(payload: Payload, _socket: RawClient) {
Payload::Text(values) => {
if let Some(first_value) = values.first() {
info!("Received doll.created event: {:?}", first_value);
if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_CREATED, first_value) {
error!("Failed to emit doll.created event: {:?}", e);
}
// Refresh dolls list
tauri::async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
});
} else {
info!("Received doll.created event with empty payload");
}
@@ -261,9 +279,31 @@ fn on_doll_updated(payload: Payload, _socket: RawClient) {
Payload::Text(values) => {
if let Some(first_value) = values.first() {
info!("Received doll.updated event: {:?}", first_value);
if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_UPDATED, first_value) {
error!("Failed to emit doll.updated event: {:?}", e);
}
// Try to extract doll ID to check if it's the active doll
let doll_id = first_value.get("id").and_then(|v| v.as_str());
let is_active_doll = if let Some(id) = doll_id {
let guard = lock_r!(FDOLL);
guard
.app_data
.user
.as_ref()
.and_then(|u| u.active_doll_id.as_ref())
.map(|active_id| active_id == id)
.unwrap_or(false)
} else {
false
};
// Refresh dolls + potentially User/Friends if active doll
tauri::async_runtime::spawn(async move {
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
if is_active_doll {
init_app_data_scoped(AppDataRefreshScope::User).await;
init_app_data_scoped(AppDataRefreshScope::Friends).await;
}
});
} else {
info!("Received doll.updated event with empty payload");
}
@@ -277,9 +317,31 @@ fn on_doll_deleted(payload: Payload, _socket: RawClient) {
Payload::Text(values) => {
if let Some(first_value) = values.first() {
info!("Received doll.deleted event: {:?}", first_value);
if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_DELETED, first_value) {
error!("Failed to emit doll.deleted event: {:?}", e);
}
// Try to extract doll ID to check if it was the active doll
let doll_id = first_value.get("id").and_then(|v| v.as_str());
let is_active_doll = if let Some(id) = doll_id {
let guard = lock_r!(FDOLL);
guard
.app_data
.user
.as_ref()
.and_then(|u| u.active_doll_id.as_ref())
.map(|active_id| active_id == id)
.unwrap_or(false)
} else {
false
};
// Refresh dolls + User/Friends if the deleted doll was active
tauri::async_runtime::spawn(async move {
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
if is_active_doll {
init_app_data_scoped(AppDataRefreshScope::User).await;
init_app_data_scoped(AppDataRefreshScope::Friends).await;
}
});
} else {
info!("Received doll.deleted event with empty payload");
}

View File

@@ -5,15 +5,16 @@ use crate::{
app_config::{AppConfig, AuthConfig},
app_data::AppData,
},
remotes::{friends::FriendRemote, user::UserRemote},
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
services::auth::{load_auth_pass, AuthPass},
};
use serde_json::json;
use std::{
collections::HashSet,
env,
sync::{Arc, LazyLock, RwLock},
};
use tauri::Emitter;
use tokio::sync::Mutex;
use tracing::{info, warn};
#[derive(Default, Clone)]
@@ -154,57 +155,188 @@ pub fn init_fdoll_state(tracing_guard: Option<tracing_appender::non_blocking::Wo
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
}
/// Defines which parts of AppData should be refreshed from the server
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AppDataRefreshScope {
/// Refresh all data (user profile + friends list + dolls list)
All,
/// Refresh only user profile
User,
/// Refresh only friends list
Friends,
/// Refresh only dolls list
Dolls,
}
/// To be called in init state or need to refresh data.
/// Populate user data in app state from the server.
///
/// This is a convenience wrapper that refreshes all data.
/// For more control, use `init_app_data_scoped`.
pub async fn init_app_data() {
let user_remote = UserRemote::new();
let friend_remote = FriendRemote::new();
init_app_data_scoped(AppDataRefreshScope::All).await;
}
// Fetch user profile
match user_remote.get_user(None).await {
Ok(user) => {
let mut guard = lock_w!(FDOLL);
guard.app_data.user = Some(user);
}
Err(e) => {
warn!("Failed to fetch user profile: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
/// Populate specific parts of app data from the server based on the scope.
///
/// # Arguments
/// * `scope` - Determines which data to refresh (All, User, or Friends)
///
/// # Examples
/// ```
/// // Refresh only friends list when a friend request is accepted
/// init_app_data_scoped(AppDataRefreshScope::Friends).await;
///
/// // Refresh only user profile when updating user settings
/// init_app_data_scoped(AppDataRefreshScope::User).await;
/// ```
static REFRESH_IN_FLIGHT: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
static REFRESH_PENDING: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Network Error",
"Failed to fetch user profile. You may be offline.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
// We continue execution to see if other parts (like friends) can be loaded or if we just stay in a partial state.
// Alternatively, return early here.
pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
loop {
// Deduplicate concurrent refreshes for the same scope
{
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
if in_flight.contains(&scope) {
let mut pending = REFRESH_PENDING.lock().await;
pending.insert(scope);
return;
}
in_flight.insert(scope);
}
}
// Fetch friends list
match friend_remote.get_friends().await {
Ok(friends) => {
let mut guard = lock_w!(FDOLL);
guard.app_data.friends = Some(friends);
}
Err(e) => {
warn!("Failed to fetch friends list: {}", e);
// Optionally show another dialog or just log it.
// Showing too many dialogs on startup is annoying, so we might skip this one if user profile failed too.
}
}
let result: Result<(), ()> = async {
let user_remote = UserRemote::new();
let friend_remote = FriendRemote::new();
let dolls_remote = DollsRemote::new();
// Emit event regardless of partial success, frontend should handle nulls/empty states
{
let guard = lock_r!(FDOLL); // Use read lock to get data
let app_data_clone = guard.app_data.clone();
drop(guard); // Drop lock before emitting to prevent potential deadlocks
// Fetch user profile if needed
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::User) {
match user_remote.get_user(None).await {
Ok(user) => {
let mut guard = lock_w!(FDOLL);
guard.app_data.user = Some(user);
}
Err(e) => {
warn!("Failed to fetch user profile: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
if let Err(e) = get_app_handle().emit("app-data-refreshed", json!(app_data_clone)) {
warn!("Failed to emit app-data-refreshed event: {}", e);
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Network Error",
"Failed to fetch user profile. You may be offline.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
return Err(());
}
}
}
// Fetch friends list if needed
if matches!(
scope,
AppDataRefreshScope::All | AppDataRefreshScope::Friends
) {
match friend_remote.get_friends().await {
Ok(friends) => {
let mut guard = lock_w!(FDOLL);
guard.app_data.friends = Some(friends);
}
Err(e) => {
warn!("Failed to fetch friends list: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Network Error",
"Failed to fetch friends list. You may be offline.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
return Err(());
}
}
}
// Fetch dolls list if needed
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Dolls) {
match dolls_remote.get_dolls().await {
Ok(dolls) => {
let mut guard = lock_w!(FDOLL);
guard.app_data.dolls = Some(dolls);
}
Err(e) => {
warn!("Failed to fetch dolls list: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Network Error",
"Failed to fetch dolls list. You may be offline.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
return Err(());
}
}
}
// Emit event regardless of partial success, frontend should handle nulls/empty states
{
let guard = lock_r!(FDOLL); // Use read lock to get data
let app_data_clone = guard.app_data.clone();
drop(guard); // Drop lock before emitting to prevent potential deadlocks
if let Err(e) = get_app_handle().emit("app-data-refreshed", &app_data_clone) {
warn!("Failed to emit app-data-refreshed event: {}", e);
use tauri_plugin_dialog::MessageDialogBuilder;
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
let handle = get_app_handle();
MessageDialogBuilder::new(
handle.dialog().clone(),
"Sync Error",
"Could not broadcast refreshed data to the UI. Some data may be stale.",
)
.kind(MessageDialogKind::Error)
.show(|_| {});
}
}
Ok(())
}
.await;
// Clear in-flight marker even on early exit
{
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
in_flight.remove(&scope);
}
// If a refresh was queued while this one was running, run again
let rerun = {
let mut pending = REFRESH_PENDING.lock().await;
pending.remove(&scope)
};
if rerun {
continue;
}
if result.is_err() {
return;
}
break;
}
}

View File

@@ -1,72 +1,19 @@
<script lang="ts">
import { onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { appData } from "../../../../events/app-data";
import type { DollDto } from "../../../../types/bindings/DollDto";
import type { UserProfile } from "../../../../types/bindings/UserProfile";
import type { AppData } from "../../../../types/bindings/AppData";
import DollsList from "./dolls-list.svelte";
let dolls: DollDto[] = [];
let user: UserProfile | null = null;
let loading = false;
let error: string | null = null;
let user: UserProfile | null = null;
let initialLoading = true;
// We still keep the focus listener as a fallback, but the websocket events should handle most updates
onMount(() => {
refreshDolls();
// Set up listeners
const unlistenCreated = listen("doll_created", (event) => {
console.log("Received doll_created event", event);
refreshDolls();
});
const unlistenUpdated = listen("doll_updated", (event) => {
console.log("Received doll_updated event", event);
refreshDolls();
});
const unlistenDeleted = listen("doll_deleted", (event) => {
console.log("Received doll_deleted event", event);
refreshDolls();
});
return async () => {
(await unlistenCreated)();
(await unlistenUpdated)();
(await unlistenDeleted)();
};
});
let isRefreshing = false;
let refreshQueued = false;
async function refreshDolls() {
if (isRefreshing) {
refreshQueued = true;
return;
}
isRefreshing = true;
loading = true;
try {
do {
refreshQueued = false;
try {
dolls = await invoke("get_dolls");
const appData: AppData = await invoke("refresh_app_data");
user = appData.user;
} catch (e) {
error = (e as Error)?.message ?? String(e);
}
} while (refreshQueued);
} finally {
loading = false;
isRefreshing = false;
}
}
// Reactive - automatically updates when appData changes
$: dolls = $appData?.dolls ?? [];
$: user = $appData?.user ?? null;
$: initialLoading = $appData === null;
async function openCreateModal() {
await invoke("open_doll_editor_window", { dollId: null });
@@ -78,19 +25,25 @@
async function handleSetActiveDoll(dollId: string) {
try {
loading = true;
await invoke("set_active_doll", { dollId });
await refreshDolls();
// No manual refresh needed - backend will refresh and emit app-data-refreshed
} catch (e) {
error = (e as Error)?.message ?? String(e);
} finally {
loading = false;
}
}
async function handleRemoveActiveDoll() {
try {
loading = true;
await invoke("remove_active_doll");
await refreshDolls();
// No manual refresh needed - backend will refresh and emit app-data-refreshed
} catch (e) {
error = (e as Error)?.message ?? String(e);
} finally {
loading = false;
}
}
</script>
@@ -106,7 +59,7 @@
<DollsList
{dolls}
{user}
{loading}
loading={loading || initialLoading}
{error}
onEditDoll={openEditModal}
onSetActiveDoll={handleSetActiveDoll}

View File

@@ -1,6 +1,7 @@
// 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 AppData = { user: UserProfile | null, friends: Array<FriendshipResponseDto> | null, scene: SceneData, };
export type AppData = { user: UserProfile | null, friends: Array<FriendshipResponseDto> | null, dolls: Array<DollDto> | null, scene: SceneData, };