diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index db135cd..b54bffc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,7 +9,11 @@ use crate::{ user::UserRemote, }, services::{ - cursor::start_cursor_tracking, doll_editor::open_doll_editor_window, + client_config_manager::{ + load_app_config, open_config_manager_window, save_app_config, AppConfig, + }, + cursor::start_cursor_tracking, + doll_editor::open_doll_editor_window, scene::open_splash_window, }, state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL}, @@ -340,6 +344,31 @@ fn restart_app() { app_handle.restart(); } +#[tauri::command] +fn get_client_config() -> AppConfig { + let mut guard = lock_w!(FDOLL); + guard.app_config = load_app_config(); + guard.app_config.clone() +} + +#[tauri::command] +fn save_client_config(config: AppConfig) -> Result<(), String> { + match save_app_config(config) { + Ok(saved) => { + let mut guard = lock_w!(FDOLL); + guard.app_config = saved; + Ok(()) + } + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +fn open_client_config_manager() -> Result<(), String> { + open_config_manager_window(); + Ok(()) +} + #[tauri::command] async fn logout_and_restart() -> Result<(), String> { crate::services::auth::logout_and_restart() @@ -392,6 +421,9 @@ pub fn run() { recolor_gif_base64, quit_app, restart_app, + get_client_config, + save_client_config, + open_client_config_manager, open_doll_editor_window, start_auth_flow, logout_and_restart diff --git a/src-tauri/src/models/app_config.rs b/src-tauri/src/models/app_config.rs deleted file mode 100644 index 6353622..0000000 --- a/src-tauri/src/models/app_config.rs +++ /dev/null @@ -1,13 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Default, Serialize, Deserialize, Clone, Debug)] -pub struct AuthConfig { - pub audience: String, - pub auth_url: String, -} - -#[derive(Default, Serialize, Deserialize, Clone, Debug)] -pub struct AppConfig { - pub api_base_url: Option, - pub auth: AuthConfig, -} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 7b37fd7..b946570 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -1,2 +1 @@ -pub mod app_config; pub mod app_data; diff --git a/src-tauri/src/services/auth.rs b/src-tauri/src/services/auth.rs index 7d81f96..6e5d556 100644 --- a/src-tauri/src/services/auth.rs +++ b/src-tauri/src/services/auth.rs @@ -395,10 +395,7 @@ pub async fn logout_and_restart() -> Result<(), OAuthError> { let guard = lock_r!(FDOLL); ( guard.auth_pass.as_ref().map(|p| p.refresh_token.clone()), - guard - .auth_pass - .as_ref() - .map(|p| p.session_state.clone()), + guard.auth_pass.as_ref().map(|p| p.session_state.clone()), guard .app_config .api_base_url diff --git a/src-tauri/src/services/client_config_manager.rs b/src-tauri/src/services/client_config_manager.rs new file mode 100644 index 0000000..2c633e1 --- /dev/null +++ b/src-tauri/src/services/client_config_manager.rs @@ -0,0 +1,178 @@ +use std::{fs, path::PathBuf}; + +use serde::{Deserialize, Serialize}; +use tauri::Manager; +use thiserror::Error; +use tracing::{error, info, warn}; +use url::Url; + +use crate::get_app_handle; + +#[derive(Default, Serialize, Deserialize, Clone, Debug)] +pub struct AuthConfig { + pub audience: String, + pub auth_url: String, +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug)] +pub struct AppConfig { + pub api_base_url: Option, + pub auth: AuthConfig, +} + +#[derive(Debug, Error)] +pub enum ClientConfigError { + #[error("failed to resolve app config dir: {0}")] + ResolvePath(tauri::Error), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("failed to parse client config: {0}")] + Parse(#[from] serde_json::Error), +} + +pub static CLIENT_CONFIG_MANAGER_WINDOW_LABEL: &str = "client_config_manager"; +const CONFIG_FILENAME: &str = "client_config.json"; +const DEFAULT_API_BASE_URL: &str = "https://api.fdolls.adamcv.com"; +const DEFAULT_AUTH_URL: &str = "https://auth.adamcv.com/realms/friendolls/protocol/openid-connect"; +const DEFAULT_JWT_AUDIENCE: &str = "friendolls-desktop"; + +fn config_file_path(app_handle: &tauri::AppHandle) -> Result { + let dir = app_handle + .path() + .app_config_dir() + .map_err(ClientConfigError::ResolvePath)?; + Ok(dir.join(CONFIG_FILENAME)) +} + +fn strip_trailing_slash(value: &str) -> String { + value.trim_end_matches('/').to_string() +} + +fn sanitize(mut config: AppConfig) -> AppConfig { + // Trim and normalize optional api_base_url + config.api_base_url = config + .api_base_url + .and_then(|v| { + let trimmed = v.trim().to_string(); + if trimmed.is_empty() { + None + } else { + // ensure scheme present and no double prefixes + if let Ok(parsed) = Url::parse(&trimmed) { + Some(strip_trailing_slash(parsed.as_str())) + } else if let Ok(parsed) = Url::parse(&format!("https://{trimmed}")) { + Some(strip_trailing_slash(parsed.as_str())) + } else { + None + } + } + }) + .or_else(|| Some(DEFAULT_API_BASE_URL.to_string())) + .map(|v| strip_trailing_slash(&v)); + + let auth_url_trimmed = config.auth.auth_url.trim(); + if auth_url_trimmed.is_empty() { + config.auth.auth_url = DEFAULT_AUTH_URL.to_string(); + } else if let Ok(parsed) = Url::parse(auth_url_trimmed) { + config.auth.auth_url = strip_trailing_slash(parsed.as_str()); + } else if let Ok(parsed) = Url::parse(&format!("https://{auth_url_trimmed}")) { + config.auth.auth_url = strip_trailing_slash(parsed.as_str()); + } else { + config.auth.auth_url = DEFAULT_AUTH_URL.to_string(); + } + + if config.auth.audience.trim().is_empty() { + config.auth.audience = DEFAULT_JWT_AUDIENCE.to_string(); + } else { + config.auth.audience = config.auth.audience.trim().to_string(); + } + + config +} + +pub fn default_app_config() -> AppConfig { + AppConfig { + api_base_url: Some(DEFAULT_API_BASE_URL.to_string()), + auth: AuthConfig { + audience: DEFAULT_JWT_AUDIENCE.to_string(), + auth_url: DEFAULT_AUTH_URL.to_string(), + }, + } +} + +pub fn load_app_config() -> AppConfig { + let app_handle = get_app_handle(); + let path = match config_file_path(app_handle) { + Ok(p) => p, + Err(e) => { + warn!("Unable to resolve client config path: {e}"); + return default_app_config(); + } + }; + + if !path.exists() { + return default_app_config(); + } + + match fs::read_to_string(&path) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(cfg) => sanitize(cfg), + Err(e) => { + warn!("Failed to parse client config, using defaults: {e}"); + default_app_config() + } + }, + Err(e) => { + warn!("Failed to read client config, using defaults: {e}"); + default_app_config() + } + } +} + +pub fn save_app_config(config: AppConfig) -> Result { + let app_handle = get_app_handle(); + let path = config_file_path(app_handle)?; + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let sanitized = sanitize(config); + let serialized = serde_json::to_string_pretty(&sanitized)?; + fs::write(&path, serialized)?; + Ok(sanitized) +} + +pub fn open_config_manager_window() { + let app_handle = get_app_handle(); + let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL); + + if let Some(window) = existing_webview_window { + if let Err(e) = window.show() { + error!("Failed to show client config manager window: {e}"); + } + return; + } + + info!("Starting client config manager..."); + let webview_window = match tauri::WebviewWindowBuilder::new( + app_handle, + CLIENT_CONFIG_MANAGER_WINDOW_LABEL, + tauri::WebviewUrl::App("/client-config-manager".into()), + ) + .title("Friendolls Client Config Manager") + .inner_size(600.0, 500.0) + .build() + { + Ok(window) => { + info!("Client config manager window builder succeeded"); + window + } + Err(e) => { + error!("Failed to build client config manager window: {}", e); + return; + } + }; + + info!("Client config manager window initialized successfully."); +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index cd3109e..d37d8ca 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,5 +1,6 @@ pub mod app_menu; pub mod auth; +pub mod client_config_manager; pub mod cursor; pub mod doll_editor; pub mod health_manager; diff --git a/src-tauri/src/services/ws.rs b/src-tauri/src/services/ws.rs index d418d76..8dbeba3 100644 --- a/src-tauri/src/services/ws.rs +++ b/src-tauri/src/services/ws.rs @@ -5,10 +5,12 @@ use tracing::{error, info}; use crate::{ get_app_handle, lock_r, lock_w, - models::app_config::AppConfig, - services::cursor::{normalized_to_absolute, CursorPosition, CursorPositions}, - services::health_manager::{close_health_manager_window, show_health_manager_with_error}, - services::scene::open_scene_window, + services::{ + client_config_manager::AppConfig, + cursor::{normalized_to_absolute, CursorPosition, CursorPositions}, + health_manager::{close_health_manager_window, show_health_manager_with_error}, + scene::open_scene_window, + }, state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; use serde::{Deserialize, Serialize}; @@ -393,17 +395,11 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) { Ok(Ok(_)) => (), Ok(Err(e)) => { error!("Failed to emit cursor report: {}", e); - show_health_manager_with_error(Some(format!( - "WebSocket emit failed: {}", - e - ))); + show_health_manager_with_error(Some(format!("WebSocket emit failed: {}", e))); } Err(e) => { error!("Failed to execute blocking task for cursor report: {}", e); - show_health_manager_with_error(Some(format!( - "WebSocket task failed: {}", - e - ))); + show_health_manager_with_error(Some(format!("WebSocket task failed: {}", e))); } } } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index f447d85..1447bb3 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,16 +1,15 @@ // in app-core/src/state.rs use crate::{ get_app_handle, lock_r, lock_w, - models::{ - app_config::{AppConfig, AuthConfig}, - app_data::AppData, - }, + models::app_data::AppData, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, - services::auth::{load_auth_pass, AuthPass}, + services::{ + auth::{load_auth_pass, AuthPass}, + client_config_manager::{load_app_config, AppConfig, AuthConfig}, + }, }; use std::{ collections::HashSet, - env, sync::{Arc, LazyLock, RwLock}, }; use tauri::Emitter; @@ -53,13 +52,7 @@ pub fn init_fdoll_state(tracing_guard: Option pass, Err(e) => { diff --git a/src/routes/app-menu/tabs/preferences.svelte b/src/routes/app-menu/tabs/preferences.svelte index bbef75e..a204f47 100644 --- a/src/routes/app-menu/tabs/preferences.svelte +++ b/src/routes/app-menu/tabs/preferences.svelte @@ -15,14 +15,27 @@ signingOut = false; } } + + const openClientConfigManager = async () => { + try { + await invoke("open_client_config_manager"); + } catch (error) { + console.error("Failed to open client config manager", error); + } + };

{$appData?.user?.name}'s preferences

- +
+ + +
diff --git a/src/routes/client-config-manager/+page.svelte b/src/routes/client-config-manager/+page.svelte new file mode 100644 index 0000000..a30d63d --- /dev/null +++ b/src/routes/client-config-manager/+page.svelte @@ -0,0 +1,109 @@ + + +
+
+

Client Configuration

+

Set custom API and auth endpoints.

+
+ +
+ + + +
+ + {#if errorMessage} +

{errorMessage}

+ {/if} + {#if successMessage} +

{successMessage}

+ {/if} + +
+ +
+
diff --git a/src/routes/health-manager/+page.svelte b/src/routes/health-manager/+page.svelte index db381e4..af16abf 100644 --- a/src/routes/health-manager/+page.svelte +++ b/src/routes/health-manager/+page.svelte @@ -34,15 +34,16 @@
-
-

Something is not right...

-

- Seems like the server is inaccessible. Check your network? -

- {#if errorMessage} -

{errorMessage}

- {/if} -
+
+

Something is not right...

+

+ Seems like the server is inaccessible. Check your network? +

+ {#if errorMessage} +

{errorMessage}

+ {/if} +
+
+
+
diff --git a/src/routes/welcome/+page.svelte b/src/routes/welcome/+page.svelte index b33cd08..19c5828 100644 --- a/src/routes/welcome/+page.svelte +++ b/src/routes/welcome/+page.svelte @@ -17,6 +17,14 @@ isContinuing = false; } }; + + const openClientConfigManager = async () => { + try { + await invoke("open_client_config_manager"); + } catch (error) { + console.error("Failed to open client config manager", error); + } + };
@@ -32,22 +40,26 @@ a cute passive socialization layer!

-
- -
+
+ + +
+

An account is needed to identify you for connecting with friends.