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) <noreply@anthropic.com>
This commit is contained in:
parent
4483a6450f
commit
a60b1a997b
|
|
@ -11,7 +11,7 @@ use tauri::{Emitter, Manager, State};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use onyx_core::{
|
use onyx_core::{
|
||||||
config::{AppConfig, WorkspaceConfig},
|
config::{AppConfig, WorkspaceConfig, WorkspaceMode},
|
||||||
models::{Task, TaskList, TaskStatus},
|
models::{Task, TaskList, TaskStatus},
|
||||||
repository::TaskRepository,
|
repository::TaskRepository,
|
||||||
sync::{self, SyncMode, SyncResult as CoreSyncResult},
|
sync::{self, SyncMode, SyncResult as CoreSyncResult},
|
||||||
|
|
@ -31,6 +31,7 @@ static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None);
|
||||||
struct AppState {
|
struct AppState {
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
config_path: PathBuf,
|
config_path: PathBuf,
|
||||||
|
app_data_dir: PathBuf,
|
||||||
repo: Option<TaskRepository>,
|
repo: Option<TaskRepository>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,6 +418,53 @@ fn set_webdav_config(
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_workspace_theme(
|
||||||
|
workspace_name: String,
|
||||||
|
theme: Option<String>,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> 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<AppState>>,
|
||||||
|
) -> 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]
|
#[tauri::command]
|
||||||
fn store_credentials(
|
fn store_credentials(
|
||||||
domain: String,
|
domain: String,
|
||||||
|
|
@ -545,23 +593,18 @@ pub fn run() {
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.setup(|app| {
|
.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 = {
|
let config_path = {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{ app_data_dir.join("config.json") }
|
||||||
use tauri::Manager;
|
|
||||||
app.path().app_data_dir()
|
|
||||||
.map_err(|e| format!("Failed to get app data dir: {}", e))?
|
|
||||||
.join("config.json")
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[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 config = AppConfig::load_from_file(&config_path).unwrap_or_default();
|
||||||
let workspace_path = config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone());
|
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"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
if let Some(path) = workspace_path {
|
if let Some(path) = workspace_path {
|
||||||
|
|
@ -591,6 +634,8 @@ pub fn run() {
|
||||||
set_group_by_due_date,
|
set_group_by_due_date,
|
||||||
get_group_by_due_date,
|
get_group_by_due_date,
|
||||||
set_webdav_config,
|
set_webdav_config,
|
||||||
|
set_workspace_theme,
|
||||||
|
add_webdav_workspace,
|
||||||
store_credentials,
|
store_credentials,
|
||||||
load_credentials,
|
load_credentials,
|
||||||
test_webdav_connection,
|
test_webdav_connection,
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,14 @@ export interface TaskList {
|
||||||
group_by_due_date: boolean;
|
group_by_due_date: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkspaceMode = "local" | "webdav";
|
||||||
|
|
||||||
export interface WorkspaceConfig {
|
export interface WorkspaceConfig {
|
||||||
path: string;
|
path: string;
|
||||||
|
mode: WorkspaceMode;
|
||||||
webdav_url: string | null;
|
webdav_url: string | null;
|
||||||
last_sync: string | null;
|
last_sync: string | null;
|
||||||
|
theme: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,35 @@ use std::path::PathBuf;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::error::{Error, Result};
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WorkspaceConfig {
|
pub struct WorkspaceConfig {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mode: WorkspaceMode,
|
||||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
pub webdav_url: Option<String>,
|
pub webdav_url: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
pub last_sync: Option<chrono::DateTime<chrono::Utc>>,
|
pub last_sync: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub theme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceConfig {
|
impl WorkspaceConfig {
|
||||||
pub fn new(path: PathBuf) -> Self {
|
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 temp_dir = TempDir::new().unwrap();
|
||||||
let config_path = temp_dir.path().join("config.json");
|
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#"{
|
let old_json = r#"{
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"personal": { "path": "/home/user/tasks" }
|
"personal": { "path": "/home/user/tasks" }
|
||||||
|
|
@ -243,5 +260,7 @@ mod tests {
|
||||||
assert_eq!(ws.path, PathBuf::from("/home/user/tasks"));
|
assert_eq!(ws.path, PathBuf::from("/home/user/tasks"));
|
||||||
assert!(ws.webdav_url.is_none());
|
assert!(ws.webdav_url.is_none());
|
||||||
assert!(ws.last_sync.is_none());
|
assert!(ws.last_sync.is_none());
|
||||||
|
assert_eq!(ws.mode, WorkspaceMode::Local);
|
||||||
|
assert!(ws.theme.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue