mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 17:37:29 +02:00
fix(notebooks): normalize icon paths across platforms
This commit is contained in:
@@ -555,15 +555,17 @@ pub fn move_notebook(
|
|||||||
|
|
||||||
// Update notebook icon mappings
|
// Update notebook icon mappings
|
||||||
if let Ok(icons) = operations::load_notebook_icons(vault_path) {
|
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 new_icons = std::collections::HashMap::new();
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
for (key, value) in &icons {
|
for (key, value) in &icons {
|
||||||
if *key == old_relative {
|
if *key == old_icon_key {
|
||||||
new_icons.insert(new_relative.clone(), value.clone());
|
new_icons.insert(new_icon_key.clone(), value.clone());
|
||||||
changed = true;
|
changed = true;
|
||||||
} else if key.starts_with(&old_prefix) {
|
} 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());
|
new_icons.insert(new_key, value.clone());
|
||||||
changed = true;
|
changed = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1218,16 +1218,25 @@ pub fn save_attachment(vault_path: &str, name: &str, data: &[u8]) -> Result<Stri
|
|||||||
Ok(relative)
|
Ok(relative)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn normalize_notebook_icon_key(path: &str) -> String {
|
||||||
|
path.replace('\\', "/")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_notebook_icons(
|
pub fn load_notebook_icons(
|
||||||
vault_path: &str,
|
vault_path: &str,
|
||||||
) -> Result<std::collections::HashMap<String, String>, String> {
|
) -> Result<std::collections::HashMap<String, String>, String> {
|
||||||
let icons_path = helixnotes_dir(vault_path).join("notebook_icons.json");
|
let icons_path = helixnotes_dir(vault_path).join("notebook_icons.json");
|
||||||
if icons_path.exists() {
|
if !icons_path.exists() {
|
||||||
let data = fs::read_to_string(&icons_path).map_err(|e| e.to_string())?;
|
return Ok(std::collections::HashMap::new());
|
||||||
serde_json::from_str(&data).map_err(|e| e.to_string())
|
|
||||||
} else {
|
|
||||||
Ok(std::collections::HashMap::new())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let data = fs::read_to_string(&icons_path).map_err(|e| e.to_string())?;
|
||||||
|
let icons: std::collections::HashMap<String, String> =
|
||||||
|
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(
|
pub fn set_notebook_icon(
|
||||||
@@ -1236,12 +1245,13 @@ pub fn set_notebook_icon(
|
|||||||
icon_relative: Option<&str>,
|
icon_relative: Option<&str>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut icons = load_notebook_icons(vault_path)?;
|
let mut icons = load_notebook_icons(vault_path)?;
|
||||||
|
let notebook_key = normalize_notebook_icon_key(notebook_relative);
|
||||||
match icon_relative {
|
match icon_relative {
|
||||||
Some(icon) => {
|
Some(icon) => {
|
||||||
icons.insert(notebook_relative.to_string(), icon.to_string());
|
icons.insert(notebook_key, icon.to_string());
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
icons.remove(notebook_relative);
|
icons.remove(¬ebook_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let icons_path = helixnotes_dir(vault_path).join("notebook_icons.json");
|
let icons_path = helixnotes_dir(vault_path).join("notebook_icons.json");
|
||||||
@@ -1380,19 +1390,44 @@ mod tests {
|
|||||||
let vault_path = vault.to_string_lossy();
|
let vault_path = vault.to_string_lossy();
|
||||||
fs::create_dir_all(helixnotes_dir(&vault_path)).unwrap();
|
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<String, String> = 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();
|
let icons = load_notebook_icons(&vault_path).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
icons.get("Projects").map(String::as_str),
|
icons.get("Projects/Client").map(String::as_str),
|
||||||
Some("builtin:briefcase")
|
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());
|
assert!(load_notebook_icons(&vault_path).unwrap().is_empty());
|
||||||
|
|
||||||
fs::remove_dir_all(vault).unwrap();
|
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]
|
#[test]
|
||||||
fn loads_only_requested_note_switcher_titles() {
|
fn loads_only_requested_note_switcher_titles() {
|
||||||
let vault =
|
let vault =
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
NOTEBOOK_ICON_OPTIONS,
|
NOTEBOOK_ICON_OPTIONS,
|
||||||
decodeBuiltinNotebookIcon,
|
decodeBuiltinNotebookIcon,
|
||||||
encodeBuiltinNotebookIcon,
|
encodeBuiltinNotebookIcon,
|
||||||
|
normalizeNotebookIconKey,
|
||||||
type NotebookIconId
|
type NotebookIconId
|
||||||
} from '$lib/utils/notebook-icons';
|
} from '$lib/utils/notebook-icons';
|
||||||
|
|
||||||
@@ -587,9 +588,10 @@
|
|||||||
|
|
||||||
async function handleBuiltinIcon(nb: NotebookEntry, icon: NotebookIconId) {
|
async function handleBuiltinIcon(nb: NotebookEntry, icon: NotebookIconId) {
|
||||||
try {
|
try {
|
||||||
|
const key = normalizeNotebookIconKey(nb.relative_path);
|
||||||
const value = encodeBuiltinNotebookIcon(icon);
|
const value = encodeBuiltinNotebookIcon(icon);
|
||||||
await setNotebookIcon(nb.relative_path, value);
|
await setNotebookIcon(key, value);
|
||||||
$notebookIcons = { ...$notebookIcons, [nb.relative_path]: value };
|
$notebookIcons = { ...$notebookIcons, [key]: value };
|
||||||
iconPickerNotebook = null;
|
iconPickerNotebook = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to set notebook icon:', e);
|
console.error('Failed to set notebook icon:', e);
|
||||||
@@ -608,8 +610,9 @@
|
|||||||
const data = await readFile(filePath);
|
const data = await readFile(filePath);
|
||||||
const fileName = baseOf(filePath) || 'icon.png';
|
const fileName = baseOf(filePath) || 'icon.png';
|
||||||
const iconRelative = await saveAttachment(`notebook-icon-${fileName}`, Array.from(data));
|
const iconRelative = await saveAttachment(`notebook-icon-${fileName}`, Array.from(data));
|
||||||
await setNotebookIcon(nb.relative_path, iconRelative);
|
const key = normalizeNotebookIconKey(nb.relative_path);
|
||||||
$notebookIcons = { ...$notebookIcons, [nb.relative_path]: iconRelative };
|
await setNotebookIcon(key, iconRelative);
|
||||||
|
$notebookIcons = { ...$notebookIcons, [key]: iconRelative };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to set notebook icon:', e);
|
console.error('Failed to set notebook icon:', e);
|
||||||
}
|
}
|
||||||
@@ -619,9 +622,10 @@
|
|||||||
contextMenu = null;
|
contextMenu = null;
|
||||||
iconPickerNotebook = null;
|
iconPickerNotebook = null;
|
||||||
try {
|
try {
|
||||||
await setNotebookIcon(nb.relative_path, null);
|
const key = normalizeNotebookIconKey(nb.relative_path);
|
||||||
|
await setNotebookIcon(key, null);
|
||||||
const icons = { ...$notebookIcons };
|
const icons = { ...$notebookIcons };
|
||||||
delete icons[nb.relative_path];
|
delete icons[key];
|
||||||
$notebookIcons = icons;
|
$notebookIcons = icons;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to remove notebook icon:', e);
|
console.error('Failed to remove notebook icon:', e);
|
||||||
@@ -629,7 +633,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNotebookIconSrc(nb: NotebookEntry): string | null {
|
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;
|
if (!iconPath || iconPath.startsWith('builtin:')) return null;
|
||||||
const vaultRoot = $appConfig?.active_vault;
|
const vaultRoot = $appConfig?.active_vault;
|
||||||
if (!vaultRoot) return null;
|
if (!vaultRoot) return null;
|
||||||
@@ -955,7 +959,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button onclick={() => openIconPicker(contextMenu!.notebook)}>
|
<button onclick={() => openIconPicker(contextMenu!.notebook)}>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" y1="9" x2="9.01" y2="9" /><line x1="15" y1="9" x2="15.01" y2="9" /></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" y1="9" x2="9.01" y2="9" /><line x1="15" y1="9" x2="15.01" y2="9" /></svg>
|
||||||
{$notebookIcons[contextMenu.notebook.relative_path] ? 'Change Icon...' : 'Set Icon...'}
|
{$notebookIcons[normalizeNotebookIconKey(contextMenu.notebook.relative_path)] ? 'Change Icon...' : 'Set Icon...'}
|
||||||
</button>
|
</button>
|
||||||
<button class="danger" onclick={() => handleDelete(contextMenu!.notebook)}>
|
<button class="danger" onclick={() => handleDelete(contextMenu!.notebook)}>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6" /><path d="M10 11v6" /><path d="M14 11v6" /><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2" /></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6" /><path d="M10 11v6" /><path d="M14 11v6" /><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2" /></svg>
|
||||||
@@ -981,7 +985,7 @@
|
|||||||
{#each NOTEBOOK_ICON_OPTIONS as option}
|
{#each NOTEBOOK_ICON_OPTIONS as option}
|
||||||
<button
|
<button
|
||||||
class="icon-picker-option"
|
class="icon-picker-option"
|
||||||
class:active={$notebookIcons[iconPickerNotebook.relative_path] === encodeBuiltinNotebookIcon(option.id)}
|
class:active={$notebookIcons[normalizeNotebookIconKey(iconPickerNotebook.relative_path)] === encodeBuiltinNotebookIcon(option.id)}
|
||||||
aria-label={option.label}
|
aria-label={option.label}
|
||||||
title={option.label}
|
title={option.label}
|
||||||
onclick={() => handleBuiltinIcon(iconPickerNotebook!, option.id)}
|
onclick={() => handleBuiltinIcon(iconPickerNotebook!, option.id)}
|
||||||
@@ -996,7 +1000,7 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="m21 15-5-5L5 21" /></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="m21 15-5-5L5 21" /></svg>
|
||||||
Custom image...
|
Custom image...
|
||||||
</button>
|
</button>
|
||||||
{#if $notebookIcons[iconPickerNotebook.relative_path]}
|
{#if $notebookIcons[normalizeNotebookIconKey(iconPickerNotebook.relative_path)]}
|
||||||
<button class="remove" onclick={() => handleRemoveIcon(iconPickerNotebook!)}>
|
<button class="remove" onclick={() => handleRemoveIcon(iconPickerNotebook!)}>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13" /></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13" /></svg>
|
||||||
Use default
|
Use default
|
||||||
@@ -1042,7 +1046,7 @@
|
|||||||
{@const hasChildren = nb.children.length > 0}
|
{@const hasChildren = nb.children.length > 0}
|
||||||
{@const isCollapsed = $collapsedNotebooks.includes(nb.path)}
|
{@const isCollapsed = $collapsedNotebooks.includes(nb.path)}
|
||||||
{@const iconSrc = getNotebookIconSrc(nb)}
|
{@const iconSrc = getNotebookIconSrc(nb)}
|
||||||
{@const builtinIcon = decodeBuiltinNotebookIcon($notebookIcons[nb.relative_path])}
|
{@const builtinIcon = decodeBuiltinNotebookIcon($notebookIcons[normalizeNotebookIconKey(nb.relative_path)])}
|
||||||
{#if editingNotebook === nb.path}
|
{#if editingNotebook === nb.path}
|
||||||
<div class="notebook-item" style="padding-left: {4 + depth * 16}px">
|
<div class="notebook-item" style="padding-left: {4 + depth * 16}px">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const BUILTIN_IDS: Record<NotebookIconId, true> = {
|
|||||||
palette: true
|
palette: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function normalizeNotebookIconKey(path: string): string {
|
||||||
|
return path.replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
export function encodeBuiltinNotebookIcon(icon: NotebookIconId): string {
|
export function encodeBuiltinNotebookIcon(icon: NotebookIconId): string {
|
||||||
return `${BUILTIN_PREFIX}${icon}`;
|
return `${BUILTIN_PREFIX}${icon}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user