From 9c53b3b5946e4bfc527facc8f07afed8d7cb63f1 Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Fri, 24 Jul 2026 15:54:12 +0200 Subject: [PATCH] Fix iOS Files vault transitions --- src-tauri/src/commands.rs | 157 +++++++++++++++--------- src-tauri/src/state.rs | 2 + src/lib/components/AppLayout.svelte | 10 +- src/lib/components/SettingsPanel.svelte | 29 +++-- src/lib/components/VaultPicker.svelte | 10 +- 5 files changed, 130 insertions(+), 78 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index c9efc47..adc9fce 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -143,7 +143,12 @@ fn open_vault_path( } #[tauri::command] -pub fn open_vault(app: AppHandle, state: State<'_, AppState>, path: String) -> Result<(), String> { +pub async fn open_vault( + app: AppHandle, + state: State<'_, AppState>, + path: String, +) -> Result<(), String> { + let _transition = state.vault_transition.lock().await; open_vault_path(app.clone(), &state, path, None)?; #[cfg(target_os = "ios")] app.ios_vault_access().release_active()?; @@ -155,6 +160,7 @@ pub async fn choose_external_vault( app: AppHandle, state: State<'_, AppState>, ) -> Result, String> { + let _transition = state.vault_transition.lock().await; #[cfg(target_os = "ios")] { let picker_app = app.clone(); @@ -194,7 +200,7 @@ pub async fn choose_external_vault( } #[cfg(not(target_os = "ios"))] { - let _ = (app, state); + let _ = (&app, &state); Err("Files folders are only available on iOS.".to_string()) } } @@ -205,6 +211,7 @@ pub async fn restore_external_vault( state: State<'_, AppState>, bookmark_id: String, ) -> Result { + let _transition = state.vault_transition.lock().await; #[cfg(target_os = "ios")] { let resolver_app = app.clone(); @@ -228,7 +235,7 @@ pub async fn restore_external_vault( } #[cfg(not(target_os = "ios"))] { - let _ = (app, state, bookmark_id); + let _ = (&app, &state, &bookmark_id); Err("Files folders are only available on iOS.".to_string()) } } @@ -240,39 +247,34 @@ pub async fn remove_vault( path: String, bookmark_id: Option, ) -> Result<(), String> { - let target = { - let config = state.config.lock().map_err(|error| error.to_string())?; - let vault = if let Some(bookmark_id) = bookmark_id.as_deref() { - config - .vaults - .iter() - .find(|vault| vault.bookmark_id.as_deref() == Some(bookmark_id)) + let _transition = state.vault_transition.lock().await; + let mut config = state.config.lock().map_err(|error| error.to_string())?; + let target = if let Some(bookmark_id) = bookmark_id.as_deref() { + config + .vaults + .iter() + .find(|vault| vault.bookmark_id.as_deref() == Some(bookmark_id)) + } else { + config + .vaults + .iter() + .find(|vault| vault.bookmark_id.is_none() && vault.path == path) + } + .map(|vault| { + let is_active = if let Some(bookmark_id) = vault.bookmark_id.as_deref() { + config.active_bookmark_id.as_deref() == Some(bookmark_id) } else { - config - .vaults - .iter() - .find(|vault| vault.bookmark_id.is_none() && vault.path == path) + config.active_bookmark_id.is_none() + && config.active_vault.as_deref() == Some(vault.path.as_str()) }; - vault.map(|vault| { - let is_active = if let Some(bookmark_id) = vault.bookmark_id.as_deref() { - config.active_bookmark_id.as_deref() == Some(bookmark_id) - } else { - config.active_bookmark_id.is_none() - && config.active_vault.as_deref() == Some(vault.path.as_str()) - }; - (vault.path.clone(), vault.bookmark_id.clone(), is_active) - }) - }; + (vault.path.clone(), vault.bookmark_id.clone(), is_active) + }); let Some((target_path, target_bookmark, is_active)) = target else { return Ok(()); }; - let old = state - .config - .lock() - .map_err(|error| error.to_string())? - .clone(); + let old = config.clone(); let mut next = old.clone(); if let Some(id) = target_bookmark.as_deref() { next.vaults @@ -289,20 +291,7 @@ pub async fn remove_vault( #[cfg(target_os = "ios")] if let Some(id) = target_bookmark.as_deref() { - let forget_app = app.clone(); - let id_to_forget = id.to_string(); - let forget_result = match tauri::async_runtime::spawn_blocking(move || { - forget_app - .ios_vault_access() - .forget_bookmark(&id_to_forget) - }) - .await - { - Ok(result) => result, - Err(error) => Err(error.to_string()), - }; - - if let Err(error) = forget_result { + if let Err(error) = app.ios_vault_access().forget_bookmark(id) { return match save_app_config(&old) { Ok(()) => Err(error), Err(rollback_error) => Err(format!( @@ -317,7 +306,7 @@ pub async fn remove_vault( if is_active { clear_vault_runtime(&state)?; } - *state.config.lock().map_err(|error| error.to_string())? = next; + *config = next; Ok(()) } @@ -2005,15 +1994,63 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> { // ── Sync (WebDAV) ── -fn active_vault_config(config: &AppConfig) -> Result<&VaultConfig, String> { +fn vault_matches_identity( + vault: &VaultConfig, + path: &str, + bookmark_id: Option<&str>, +) -> bool { + if let Some(bookmark_id) = bookmark_id { + vault.bookmark_id.as_deref() == Some(bookmark_id) + } else { + vault.bookmark_id.is_none() && vault.path == path + } +} + +fn active_vault_index(config: &AppConfig) -> Result { let active = config.active_vault.as_deref().ok_or("No active vault")?; config .vaults .iter() - .find(|v| v.path == active) + .position(|vault| { + vault_matches_identity(vault, active, config.active_bookmark_id.as_deref()) + }) .ok_or_else(|| "Active vault not found in config".to_string()) } +fn active_vault_config(config: &AppConfig) -> Result<&VaultConfig, String> { + Ok(&config.vaults[active_vault_index(config)?]) +} + +#[cfg(test)] +mod vault_identity_tests { + use super::*; + + #[test] + fn bookmark_identity_disambiguates_vaults_with_the_same_path() { + let mut config = AppConfig::default(); + config.active_vault = Some("/same/path".to_string()); + config.vaults = vec![ + VaultConfig { + path: "/same/path".to_string(), + name: "Local".to_string(), + ..Default::default() + }, + VaultConfig { + path: "/same/path".to_string(), + name: "Files".to_string(), + bookmark_id: Some("bookmark".to_string()), + ..Default::default() + }, + ]; + + config.active_bookmark_id = Some("bookmark".to_string()); + assert_eq!(active_vault_config(&config).unwrap().name, "Files"); + + config.active_bookmark_id = None; + assert_eq!(active_vault_config(&config).unwrap().name, "Local"); + } +} + fn sync_config_from(config: &AppConfig) -> Result { let v = active_vault_config(config)?; if v.sync_provider.as_deref() != Some("webdav") { @@ -2043,12 +2080,8 @@ pub fn set_sync_settings( sync_interval_minutes: u32, ) -> Result<(), String> { let mut config = state.config.lock().map_err(|e| e.to_string())?; - let active = config.active_vault.clone().ok_or("No active vault")?; - let v = config - .vaults - .iter_mut() - .find(|v| v.path == active) - .ok_or_else(|| "Active vault not found in config".to_string())?; + let active_index = active_vault_index(&config)?; + let v = &mut config.vaults[active_index]; v.sync_provider = provider.filter(|p| !p.is_empty()); v.webdav_url = url.filter(|u| !u.trim().is_empty()); v.webdav_username = username.filter(|u| !u.is_empty()); @@ -2094,7 +2127,7 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> { if app.state::().syncing.swap(true, Ordering::SeqCst) { return Ok(()); // a sync is already running } - let (vault, cfg) = { + let (vault, bookmark_id, cfg) = { let state = app.state::(); let config = match state.config.lock() { Ok(c) => c, @@ -2107,9 +2140,13 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> { .active_vault .clone() .ok_or_else(|| "No active vault".to_string()) - .and_then(|v| sync_config_from(&config).map(|c| (v, c))); + .and_then(|vault| { + sync_config_from(&config).map(|cfg| { + (vault, config.active_bookmark_id.clone(), cfg) + }) + }); match gathered { - Ok(vc) => vc, + Ok(vault_config) => vault_config, Err(e) => { drop(config); state.syncing.store(false, Ordering::SeqCst); @@ -2125,8 +2162,10 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> { Ok(summary) => { let ts = chrono::Utc::now().to_rfc3339(); if let Ok(mut config) = app.state::().config.lock() { - if let Some(v) = config.vaults.iter_mut().find(|v| v.path == vault) { - v.last_sync_time = Some(ts.clone()); + if let Some(vault_config) = config.vaults.iter_mut().find(|candidate| { + vault_matches_identity(candidate, &vault, bookmark_id.as_deref()) + }) { + vault_config.last_sync_time = Some(ts.clone()); } let _ = save_app_config(&config); } @@ -2265,7 +2304,7 @@ fn migrate_global_sync_to_vault(config: &mut AppConfig) -> bool { if config.sync_provider.is_none() && config.webdav_url.is_none() { return false; } - let Some(active) = config.active_vault.clone() else { return false; }; + let Ok(active_index) = active_vault_index(config) else { return false; }; let g_provider = config.sync_provider.clone(); let g_url = config.webdav_url.clone(); let g_user = config.webdav_username.clone(); @@ -2274,7 +2313,7 @@ fn migrate_global_sync_to_vault(config: &mut AppConfig) -> bool { let g_on_change = config.sync_on_change; let g_interval = config.sync_interval_minutes; let g_last = config.last_sync_time.clone(); - let Some(v) = config.vaults.iter_mut().find(|v| v.path == active) else { return false; }; + let v = &mut config.vaults[active_index]; if v.sync_provider.is_some() || v.webdav_url.is_some() { return false; // already migrated / has its own config } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 203853c..3c3e0de 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -9,6 +9,7 @@ pub struct AppState { pub config: Mutex, pub search_index: Mutex>>, pub watcher: Mutex>, + pub vault_transition: tokio::sync::Mutex<()>, pub importing: AtomicBool, pub syncing: AtomicBool, pub pending_open_file: Mutex>, @@ -20,6 +21,7 @@ impl AppState { config: Mutex::new(config), search_index: Mutex::new(None), watcher: Mutex::new(None), + vault_transition: tokio::sync::Mutex::new(()), importing: AtomicBool::new(false), syncing: AtomicBool::new(false), pending_open_file: Mutex::new(None), diff --git a/src/lib/components/AppLayout.svelte b/src/lib/components/AppLayout.svelte index 0517f83..12c3653 100644 --- a/src/lib/components/AppLayout.svelte +++ b/src/lib/components/AppLayout.svelte @@ -61,7 +61,7 @@ const appWindow = getCurrentWindow(); const isMac = navigator.platform.startsWith('Mac'); const isMobile = $derived($platformIsMobile); - import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api'; + import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, getAppConfig, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api'; import { darkThemes, isAndroid } from '$lib/platform'; import { debounce } from '$lib/utils/debounce'; import { openNoteWindow } from '$lib/utils/window'; @@ -729,11 +729,11 @@ // ── WebDAV sync: global status + auto-sync triggers ── unlistenSync.push(await listen('sync-progress', () => syncState.set({ running: true, error: null }))); - unlistenSync.push(await listen('sync-done', (event: any) => { + unlistenSync.push(await listen('sync-done', async () => { syncState.set({ running: false, error: null }); - const cur = get(appConfig); - const ts = event.payload?.last_sync_time; - if (cur && ts) appConfig.set({ ...cur, vaults: cur.vaults.map((v) => v.path === cur.active_vault ? { ...v, last_sync_time: ts } : v) }); + try { + appConfig.set(await getAppConfig()); + } catch {} })); unlistenSync.push(await listen('sync-error', (event: any) => syncState.set({ running: false, error: event.payload?.error ?? 'Sync failed' }))); diff --git a/src/lib/components/SettingsPanel.svelte b/src/lib/components/SettingsPanel.svelte index e2a6f21..73dfb2b 100644 --- a/src/lib/components/SettingsPanel.svelte +++ b/src/lib/components/SettingsPanel.svelte @@ -1,6 +1,6 @@