From a60b1a997bbbdf0db85f091afc8f0972e675d2be Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Fri, 3 Apr 2026 03:58:01 -0700 Subject: [PATCH] feat: add WorkspaceMode (local/webdav) and per-workspace theme to config Introduces WorkspaceMode enum with local and webdav variants, plus a theme field on WorkspaceConfig. Adds set_workspace_theme and add_webdav_workspace Tauri commands. WebDAV workspaces auto-manage local files in app data dir. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/tauri/src-tauri/src/lib.rs | 69 +++++++++++++++++++++++++++------ apps/tauri/src/lib/types.ts | 4 ++ crates/onyx-core/src/config.rs | 23 ++++++++++- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 1242a33..3c4efe8 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -11,7 +11,7 @@ use tauri::{Emitter, Manager, State}; use uuid::Uuid; use onyx_core::{ - config::{AppConfig, WorkspaceConfig}, + config::{AppConfig, WorkspaceConfig, WorkspaceMode}, models::{Task, TaskList, TaskStatus}, repository::TaskRepository, sync::{self, SyncMode, SyncResult as CoreSyncResult}, @@ -31,6 +31,7 @@ static LAST_WRITE: Mutex> = Mutex::new(None); struct AppState { config: AppConfig, config_path: PathBuf, + app_data_dir: PathBuf, repo: Option, } @@ -417,6 +418,53 @@ fn set_webdav_config( .map_err(|e| e.to_string()) } +#[tauri::command] +fn set_workspace_theme( + workspace_name: String, + theme: Option, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = lock_state(&state)?; + if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) { + ws.theme = theme; + } + s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn add_webdav_workspace( + name: String, + webdav_url: String, + username: String, + password: String, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = lock_state(&state)?; + let managed_dir = s.app_data_dir.join("workspaces").join(&name); + std::fs::create_dir_all(&managed_dir).map_err(|e| e.to_string())?; + TaskRepository::init(managed_dir.clone()).map(|_| ()).map_err(|e| e.to_string())?; + + let mut ws = WorkspaceConfig::new(managed_dir); + ws.mode = WorkspaceMode::Webdav; + ws.webdav_url = Some(webdav_url.clone()); + + s.config.add_workspace(name.clone(), ws); + s.config.set_current_workspace(name).map_err(|e| e.to_string())?; + s.repo = None; + + // Store credentials keyed by hostname + let domain = webdav_url + .split("://") + .nth(1) + .and_then(|rest| rest.split('/').next()) + .unwrap_or("") + .to_string(); + s.config.save_to_file(&s.config_path.clone()).map_err(|e| e.to_string())?; + drop(s); + webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())?; + Ok(()) +} + #[tauri::command] fn store_credentials( domain: String, @@ -545,23 +593,18 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) .setup(|app| { - // Resolve config path: Tauri's app_data_dir on Android, directories crate on desktop + // Resolve app data dir and config path + let app_data_dir = app.path().app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; let config_path = { #[cfg(target_os = "android")] - { - use tauri::Manager; - app.path().app_data_dir() - .map_err(|e| format!("Failed to get app data dir: {}", e))? - .join("config.json") - } + { app_data_dir.join("config.json") } #[cfg(not(target_os = "android"))] - { - AppConfig::get_config_path() - } + { AppConfig::get_config_path() } }; let config = AppConfig::load_from_file(&config_path).unwrap_or_default(); let workspace_path = config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone()); - app.manage(Mutex::new(AppState { config, config_path, repo: None })); + app.manage(Mutex::new(AppState { config, config_path, app_data_dir, repo: None })); #[cfg(not(target_os = "android"))] if let Some(path) = workspace_path { @@ -591,6 +634,8 @@ pub fn run() { set_group_by_due_date, get_group_by_due_date, set_webdav_config, + set_workspace_theme, + add_webdav_workspace, store_credentials, load_credentials, test_webdav_connection, diff --git a/apps/tauri/src/lib/types.ts b/apps/tauri/src/lib/types.ts index 40187e7..a24855c 100644 --- a/apps/tauri/src/lib/types.ts +++ b/apps/tauri/src/lib/types.ts @@ -19,10 +19,14 @@ export interface TaskList { group_by_due_date: boolean; } +export type WorkspaceMode = "local" | "webdav"; + export interface WorkspaceConfig { path: string; + mode: WorkspaceMode; webdav_url: string | null; last_sync: string | null; + theme: string | null; } export interface AppConfig { diff --git a/crates/onyx-core/src/config.rs b/crates/onyx-core/src/config.rs index b13a2bd..8d22016 100644 --- a/crates/onyx-core/src/config.rs +++ b/crates/onyx-core/src/config.rs @@ -3,18 +3,35 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum WorkspaceMode { + Local, + Webdav, +} + +impl Default for WorkspaceMode { + fn default() -> Self { + Self::Local + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkspaceConfig { pub path: PathBuf, + #[serde(default)] + pub mode: WorkspaceMode, #[serde(skip_serializing_if = "Option::is_none", default)] pub webdav_url: Option, #[serde(skip_serializing_if = "Option::is_none", default)] pub last_sync: Option>, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub theme: Option, } impl WorkspaceConfig { pub fn new(path: PathBuf) -> Self { - Self { path, webdav_url: None, last_sync: None } + Self { path, mode: WorkspaceMode::Local, webdav_url: None, last_sync: None, theme: None } } } @@ -229,7 +246,7 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.json"); - // Write old-format JSON without webdav_url or last_sync fields + // Write old-format JSON without webdav_url, last_sync, mode, or theme fields let old_json = r#"{ "workspaces": { "personal": { "path": "/home/user/tasks" } @@ -243,5 +260,7 @@ mod tests { assert_eq!(ws.path, PathBuf::from("/home/user/tasks")); assert!(ws.webdav_url.is_none()); assert!(ws.last_sync.is_none()); + assert_eq!(ws.mode, WorkspaceMode::Local); + assert!(ws.theme.is_none()); } }