trivial enhancements to state management solution
This commit is contained in:
@@ -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,11 +203,10 @@ 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.
|
||||
// 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)
|
||||
};
|
||||
|
||||
if let Err(e) = get_app_handle().emit("doll_deleted", ()) {
|
||||
tracing::error!("Failed to emit local doll.deleted event: {}", e);
|
||||
// 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]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -83,7 +83,10 @@ 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| {
|
||||
@@ -92,7 +95,10 @@ impl DollsRemote {
|
||||
})?;
|
||||
|
||||
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
|
||||
})?;
|
||||
|
||||
@@ -101,13 +107,19 @@ impl DollsRemote {
|
||||
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?;
|
||||
|
||||
@@ -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
|
||||
})?;
|
||||
|
||||
@@ -131,7 +146,10 @@ impl DollsRemote {
|
||||
|
||||
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
|
||||
@@ -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
|
||||
})?;
|
||||
|
||||
@@ -159,7 +180,10 @@ impl DollsRemote {
|
||||
|
||||
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
|
||||
@@ -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
|
||||
})?;
|
||||
|
||||
@@ -187,7 +214,10 @@ impl DollsRemote {
|
||||
|
||||
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?;
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod dolls;
|
||||
pub mod friends;
|
||||
pub mod user;
|
||||
pub mod dolls;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -136,10 +136,7 @@ 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) {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,13 +155,66 @@ 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() {
|
||||
init_app_data_scoped(AppDataRefreshScope::All).await;
|
||||
}
|
||||
|
||||
/// 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()));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let result: Result<(), ()> = async {
|
||||
let user_remote = UserRemote::new();
|
||||
let friend_remote = FriendRemote::new();
|
||||
let dolls_remote = DollsRemote::new();
|
||||
|
||||
// Fetch user profile
|
||||
// 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);
|
||||
@@ -179,12 +233,16 @@ pub async fn init_app_data() {
|
||||
)
|
||||
.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.
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch friends list
|
||||
// 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);
|
||||
@@ -192,8 +250,44 @@ pub async fn init_app_data() {
|
||||
}
|
||||
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.
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,8 +297,46 @@ pub async fn init_app_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", json!(app_data_clone)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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, };
|
||||
|
||||
Reference in New Issue
Block a user