diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 5eee2d0..dffd096 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -555,15 +555,17 @@ pub fn move_notebook( // Update notebook icon mappings if let Ok(icons) = operations::load_notebook_icons(vault_path) { - let old_prefix = format!("{}/", old_relative); + let old_icon_key = operations::normalize_notebook_icon_key(&old_relative); + let new_icon_key = operations::normalize_notebook_icon_key(&new_relative); + let old_prefix = format!("{}/", old_icon_key); let mut new_icons = std::collections::HashMap::new(); let mut changed = false; for (key, value) in &icons { - if *key == old_relative { - new_icons.insert(new_relative.clone(), value.clone()); + if *key == old_icon_key { + new_icons.insert(new_icon_key.clone(), value.clone()); changed = true; } else if key.starts_with(&old_prefix) { - let new_key = format!("{}/{}", new_relative, &key[old_prefix.len()..]); + let new_key = format!("{}/{}", new_icon_key, &key[old_prefix.len()..]); new_icons.insert(new_key, value.clone()); changed = true; } else { diff --git a/src-tauri/src/vault/operations.rs b/src-tauri/src/vault/operations.rs index a6a8f66..1dbd189 100644 --- a/src-tauri/src/vault/operations.rs +++ b/src-tauri/src/vault/operations.rs @@ -1218,16 +1218,25 @@ pub fn save_attachment(vault_path: &str, name: &str, data: &[u8]) -> Result String { + path.replace('\\', "/") +} + pub fn load_notebook_icons( vault_path: &str, ) -> Result, String> { let icons_path = helixnotes_dir(vault_path).join("notebook_icons.json"); - if icons_path.exists() { - let data = fs::read_to_string(&icons_path).map_err(|e| e.to_string())?; - serde_json::from_str(&data).map_err(|e| e.to_string()) - } else { - Ok(std::collections::HashMap::new()) + if !icons_path.exists() { + return Ok(std::collections::HashMap::new()); } + + let data = fs::read_to_string(&icons_path).map_err(|e| e.to_string())?; + let icons: std::collections::HashMap = + serde_json::from_str(&data).map_err(|e| e.to_string())?; + Ok(icons + .into_iter() + .map(|(path, icon)| (normalize_notebook_icon_key(&path), icon)) + .collect()) } pub fn set_notebook_icon( @@ -1236,12 +1245,13 @@ pub fn set_notebook_icon( icon_relative: Option<&str>, ) -> Result<(), String> { let mut icons = load_notebook_icons(vault_path)?; + let notebook_key = normalize_notebook_icon_key(notebook_relative); match icon_relative { Some(icon) => { - icons.insert(notebook_relative.to_string(), icon.to_string()); + icons.insert(notebook_key, icon.to_string()); } None => { - icons.remove(notebook_relative); + icons.remove(¬ebook_key); } } let icons_path = helixnotes_dir(vault_path).join("notebook_icons.json"); @@ -1380,19 +1390,44 @@ mod tests { let vault_path = vault.to_string_lossy(); fs::create_dir_all(helixnotes_dir(&vault_path)).unwrap(); - set_notebook_icon(&vault_path, "Projects", Some("builtin:briefcase")).unwrap(); + set_notebook_icon(&vault_path, r"Projects\Client", Some("builtin:briefcase")).unwrap(); + let stored: std::collections::HashMap = serde_json::from_str( + &fs::read_to_string(helixnotes_dir(&vault_path).join("notebook_icons.json")).unwrap(), + ) + .unwrap(); + assert!(stored.contains_key("Projects/Client")); + assert!(!stored.contains_key(r"Projects\Client")); + let icons = load_notebook_icons(&vault_path).unwrap(); assert_eq!( - icons.get("Projects").map(String::as_str), + icons.get("Projects/Client").map(String::as_str), Some("builtin:briefcase") ); - set_notebook_icon(&vault_path, "Projects", None).unwrap(); + set_notebook_icon(&vault_path, "Projects/Client", None).unwrap(); assert!(load_notebook_icons(&vault_path).unwrap().is_empty()); fs::remove_dir_all(vault).unwrap(); } + #[test] + fn normalizes_legacy_notebook_icon_keys_when_loading() { + let vault = + std::env::temp_dir().join(format!("helixnotes-notebook-icon-test-{}", Uuid::new_v4())); + let vault_path = vault.to_string_lossy(); + let icons_path = helixnotes_dir(&vault_path).join("notebook_icons.json"); + fs::create_dir_all(helixnotes_dir(&vault_path)).unwrap(); + fs::write(&icons_path, r#"{"Projects\\Client":"builtin:folder"}"#).unwrap(); + + let icons = load_notebook_icons(&vault_path).unwrap(); + assert_eq!( + icons.get("Projects/Client").map(String::as_str), + Some("builtin:folder") + ); + + fs::remove_dir_all(vault).unwrap(); + } + #[test] fn loads_only_requested_note_switcher_titles() { let vault = diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 66e3dc5..e7e7ea5 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -33,6 +33,7 @@ NOTEBOOK_ICON_OPTIONS, decodeBuiltinNotebookIcon, encodeBuiltinNotebookIcon, + normalizeNotebookIconKey, type NotebookIconId } from '$lib/utils/notebook-icons'; @@ -587,9 +588,10 @@ async function handleBuiltinIcon(nb: NotebookEntry, icon: NotebookIconId) { try { + const key = normalizeNotebookIconKey(nb.relative_path); const value = encodeBuiltinNotebookIcon(icon); - await setNotebookIcon(nb.relative_path, value); - $notebookIcons = { ...$notebookIcons, [nb.relative_path]: value }; + await setNotebookIcon(key, value); + $notebookIcons = { ...$notebookIcons, [key]: value }; iconPickerNotebook = null; } catch (e) { console.error('Failed to set notebook icon:', e); @@ -608,8 +610,9 @@ const data = await readFile(filePath); const fileName = baseOf(filePath) || 'icon.png'; const iconRelative = await saveAttachment(`notebook-icon-${fileName}`, Array.from(data)); - await setNotebookIcon(nb.relative_path, iconRelative); - $notebookIcons = { ...$notebookIcons, [nb.relative_path]: iconRelative }; + const key = normalizeNotebookIconKey(nb.relative_path); + await setNotebookIcon(key, iconRelative); + $notebookIcons = { ...$notebookIcons, [key]: iconRelative }; } catch (e) { console.error('Failed to set notebook icon:', e); } @@ -619,9 +622,10 @@ contextMenu = null; iconPickerNotebook = null; try { - await setNotebookIcon(nb.relative_path, null); + const key = normalizeNotebookIconKey(nb.relative_path); + await setNotebookIcon(key, null); const icons = { ...$notebookIcons }; - delete icons[nb.relative_path]; + delete icons[key]; $notebookIcons = icons; } catch (e) { console.error('Failed to remove notebook icon:', e); @@ -629,7 +633,7 @@ } function getNotebookIconSrc(nb: NotebookEntry): string | null { - const iconPath = $notebookIcons[nb.relative_path]; + const iconPath = $notebookIcons[normalizeNotebookIconKey(nb.relative_path)]; if (!iconPath || iconPath.startsWith('builtin:')) return null; const vaultRoot = $appConfig?.active_vault; if (!vaultRoot) return null; @@ -955,7 +959,7 @@ - {#if $notebookIcons[iconPickerNotebook.relative_path]} + {#if $notebookIcons[normalizeNotebookIconKey(iconPickerNotebook.relative_path)]}