client configuration manager
This commit is contained in:
@@ -9,7 +9,11 @@ use crate::{
|
|||||||
user::UserRemote,
|
user::UserRemote,
|
||||||
},
|
},
|
||||||
services::{
|
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,
|
scene::open_splash_window,
|
||||||
},
|
},
|
||||||
state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||||
@@ -340,6 +344,31 @@ fn restart_app() {
|
|||||||
app_handle.restart();
|
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]
|
#[tauri::command]
|
||||||
async fn logout_and_restart() -> Result<(), String> {
|
async fn logout_and_restart() -> Result<(), String> {
|
||||||
crate::services::auth::logout_and_restart()
|
crate::services::auth::logout_and_restart()
|
||||||
@@ -392,6 +421,9 @@ pub fn run() {
|
|||||||
recolor_gif_base64,
|
recolor_gif_base64,
|
||||||
quit_app,
|
quit_app,
|
||||||
restart_app,
|
restart_app,
|
||||||
|
get_client_config,
|
||||||
|
save_client_config,
|
||||||
|
open_client_config_manager,
|
||||||
open_doll_editor_window,
|
open_doll_editor_window,
|
||||||
start_auth_flow,
|
start_auth_flow,
|
||||||
logout_and_restart
|
logout_and_restart
|
||||||
|
|||||||
@@ -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<String>,
|
|
||||||
pub auth: AuthConfig,
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
pub mod app_config;
|
|
||||||
pub mod app_data;
|
pub mod app_data;
|
||||||
|
|||||||
@@ -395,10 +395,7 @@ pub async fn logout_and_restart() -> Result<(), OAuthError> {
|
|||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
(
|
(
|
||||||
guard.auth_pass.as_ref().map(|p| p.refresh_token.clone()),
|
guard.auth_pass.as_ref().map(|p| p.refresh_token.clone()),
|
||||||
guard
|
guard.auth_pass.as_ref().map(|p| p.session_state.clone()),
|
||||||
.auth_pass
|
|
||||||
.as_ref()
|
|
||||||
.map(|p| p.session_state.clone()),
|
|
||||||
guard
|
guard
|
||||||
.app_config
|
.app_config
|
||||||
.api_base_url
|
.api_base_url
|
||||||
|
|||||||
178
src-tauri/src/services/client_config_manager.rs
Normal file
178
src-tauri/src/services/client_config_manager.rs
Normal file
@@ -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<String>,
|
||||||
|
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<PathBuf, ClientConfigError> {
|
||||||
|
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::<AppConfig>(&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<AppConfig, ClientConfigError> {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod app_menu;
|
pub mod app_menu;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod client_config_manager;
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
pub mod doll_editor;
|
pub mod doll_editor;
|
||||||
pub mod health_manager;
|
pub mod health_manager;
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ use tracing::{error, info};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
models::app_config::AppConfig,
|
services::{
|
||||||
services::cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
|
client_config_manager::AppConfig,
|
||||||
services::health_manager::{close_health_manager_window, show_health_manager_with_error},
|
cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
|
||||||
services::scene::open_scene_window,
|
health_manager::{close_health_manager_window, show_health_manager_with_error},
|
||||||
|
scene::open_scene_window,
|
||||||
|
},
|
||||||
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -393,17 +395,11 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
|||||||
Ok(Ok(_)) => (),
|
Ok(Ok(_)) => (),
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
error!("Failed to emit cursor report: {}", e);
|
error!("Failed to emit cursor report: {}", e);
|
||||||
show_health_manager_with_error(Some(format!(
|
show_health_manager_with_error(Some(format!("WebSocket emit failed: {}", e)));
|
||||||
"WebSocket emit failed: {}",
|
|
||||||
e
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to execute blocking task for cursor report: {}", e);
|
error!("Failed to execute blocking task for cursor report: {}", e);
|
||||||
show_health_manager_with_error(Some(format!(
|
show_health_manager_with_error(Some(format!("WebSocket task failed: {}", e)));
|
||||||
"WebSocket task failed: {}",
|
|
||||||
e
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
// in app-core/src/state.rs
|
// in app-core/src/state.rs
|
||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
models::{
|
models::app_data::AppData,
|
||||||
app_config::{AppConfig, AuthConfig},
|
|
||||||
app_data::AppData,
|
|
||||||
},
|
|
||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
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::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
env,
|
|
||||||
sync::{Arc, LazyLock, RwLock},
|
sync::{Arc, LazyLock, RwLock},
|
||||||
};
|
};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
@@ -53,13 +52,7 @@ pub fn init_fdoll_state(tracing_guard: Option<tracing_appender::non_blocking::Wo
|
|||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
guard.tracing_guard = tracing_guard;
|
guard.tracing_guard = tracing_guard;
|
||||||
guard.app_config = AppConfig {
|
guard.app_config = load_app_config();
|
||||||
api_base_url: Some(env::var("API_BASE_URL").expect("API_BASE_URL must be set")),
|
|
||||||
auth: AuthConfig {
|
|
||||||
audience: env::var("JWT_AUDIENCE").expect("JWT_AUDIENCE must be set"),
|
|
||||||
auth_url: env::var("AUTH_URL").expect("AUTH_URL must be set"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
guard.auth_pass = match load_auth_pass() {
|
guard.auth_pass = match load_auth_pass() {
|
||||||
Ok(pass) => pass,
|
Ok(pass) => pass,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -15,14 +15,27 @@
|
|||||||
signingOut = false;
|
signingOut = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openClientConfigManager = async () => {
|
||||||
|
try {
|
||||||
|
await invoke("open_client_config_manager");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open client config manager", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="size-full flex flex-col justify-between">
|
<div class="size-full flex flex-col justify-between">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p>{$appData?.user?.name}'s preferences</p>
|
<p>{$appData?.user?.name}'s preferences</p>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
<button class="btn" class:btn-disabled={signingOut} onclick={handleSignOut}>
|
<button class="btn" class:btn-disabled={signingOut} onclick={handleSignOut}>
|
||||||
{signingOut ? "Signing out..." : "Sign out"}
|
{signingOut ? "Signing out..." : "Sign out"}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline" onclick={openClientConfigManager}>
|
||||||
|
Advanced options
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row justify-between">
|
<div class="w-full flex flex-row justify-between">
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|||||||
109
src/routes/client-config-manager/+page.svelte
Normal file
109
src/routes/client-config-manager/+page.svelte
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
type AuthConfig = {
|
||||||
|
audience: string;
|
||||||
|
auth_url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppConfig = {
|
||||||
|
api_base_url?: string | null;
|
||||||
|
auth: AuthConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
let form: AppConfig = {
|
||||||
|
api_base_url: "",
|
||||||
|
auth: { audience: "", auth_url: "" },
|
||||||
|
};
|
||||||
|
|
||||||
|
let saving = false;
|
||||||
|
let errorMessage = "";
|
||||||
|
let successMessage = "";
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const config = (await invoke("get_client_config")) as AppConfig;
|
||||||
|
form = {
|
||||||
|
api_base_url: config.api_base_url ?? "",
|
||||||
|
auth: {
|
||||||
|
audience: config.auth.audience,
|
||||||
|
auth_url: config.auth.auth_url,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage = `Failed to load config: ${err}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (saving) return;
|
||||||
|
saving = true;
|
||||||
|
errorMessage = "";
|
||||||
|
successMessage = "";
|
||||||
|
try {
|
||||||
|
await invoke("save_client_config", {
|
||||||
|
config: {
|
||||||
|
api_base_url: form.api_base_url?.trim() || null,
|
||||||
|
auth: {
|
||||||
|
audience: form.auth.audience.trim(),
|
||||||
|
auth_url: form.auth.auth_url.trim(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
successMessage = "Configuration saved. Please restart the app.";
|
||||||
|
await invoke("restart_app");
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage = `Failed to save config: ${err}`;
|
||||||
|
} finally {
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(loadConfig);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-6 flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<p class="text-xl font-semibold">Client Configuration</p>
|
||||||
|
<p class="opacity-70 text-sm">Set custom API and auth endpoints.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">API Base URL</span>
|
||||||
|
<input
|
||||||
|
class="input input-bordered"
|
||||||
|
bind:value={form.api_base_url}
|
||||||
|
placeholder="https://api.fdolls.adamcv.com"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">Auth URL</span>
|
||||||
|
<input class="input input-bordered" bind:value={form.auth.auth_url} />
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm">JWT Audience</span>
|
||||||
|
<input class="input input-bordered" bind:value={form.auth.audience} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<p class="text-sm text-error">{errorMessage}</p>
|
||||||
|
{/if}
|
||||||
|
{#if successMessage}
|
||||||
|
<p class="text-sm text-success">{successMessage}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
class:btn-disabled={saving}
|
||||||
|
disabled={saving}
|
||||||
|
on:click={save}
|
||||||
|
>
|
||||||
|
{saving ? "Saving..." : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
<p class="text-sm opacity-70 wrap-break-word">{errorMessage}</p>
|
<p class="text-sm opacity-70 wrap-break-word">{errorMessage}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
class:btn-disabled={isRestarting}
|
class:btn-disabled={isRestarting}
|
||||||
@@ -55,5 +56,10 @@
|
|||||||
Try again
|
Try again
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline" onclick={async () => invoke("open_client_config_manager")}>
|
||||||
|
Advanced options
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
isContinuing = false;
|
isContinuing = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openClientConfigManager = async () => {
|
||||||
|
try {
|
||||||
|
await invoke("open_client_config_manager");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open client config manager", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="size-full relative bg-linear-to-br from-base-100 to-[#b7f2ff77]">
|
<div class="size-full relative bg-linear-to-br from-base-100 to-[#b7f2ff77]">
|
||||||
@@ -32,7 +40,7 @@
|
|||||||
a cute passive socialization layer!
|
a cute passive socialization layer!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
onclick={handleContinue}
|
onclick={handleContinue}
|
||||||
@@ -47,7 +55,11 @@
|
|||||||
Sign in with browser
|
Sign in with browser
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline" onclick={openClientConfigManager}>
|
||||||
|
Advanced options
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-xs opacity-50 max-w-60">
|
<p class="text-xs opacity-50 max-w-60">
|
||||||
An account is needed to identify you for connecting with friends.
|
An account is needed to identify you for connecting with friends.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user