Skip to content

Commit 523613b

Browse files
committed
Polish sidebar customization and layout
1 parent 2faab39 commit 523613b

16 files changed

Lines changed: 1428 additions & 254 deletions

File tree

apps/desktop/src/main/vault.ts

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { app } from 'electron'
77
import {
88
DEFAULT_DAILY_NOTES_DIRECTORY,
99
AssetMeta,
10+
type FolderIconId,
1011
type PrimaryNotesLocation,
1112
type VaultSettings,
1213
FolderEntry,
@@ -64,13 +65,45 @@ const SEARCH_EXECUTABLE_NAMES = {
6465
ripgrep: new Set(['rg', 'rg.exe']),
6566
fzf: new Set(['fzf', 'fzf.exe'])
6667
} as const
68+
const VALID_FOLDER_ICON_IDS = new Set<FolderIconId>([
69+
'folder',
70+
'bolt',
71+
'tray',
72+
'archive',
73+
'trash',
74+
'book',
75+
'bookmark',
76+
'calendar',
77+
'briefcase',
78+
'tag',
79+
'document',
80+
'sparkle',
81+
'code',
82+
'user',
83+
'star',
84+
'heart',
85+
'link',
86+
'lightbulb',
87+
'flask',
88+
'graduation',
89+
'music',
90+
'image',
91+
'palette',
92+
'terminal',
93+
'wrench',
94+
'globe',
95+
'map',
96+
'chart',
97+
'home'
98+
])
6799

68100
const DEFAULT_VAULT_SETTINGS: VaultSettings = {
69101
primaryNotesLocation: 'inbox',
70102
dailyNotes: {
71103
enabled: false,
72104
directory: DEFAULT_DAILY_NOTES_DIRECTORY
73-
}
105+
},
106+
folderIcons: {}
74107
}
75108

76109
interface VaultTextSearchCandidate {
@@ -286,7 +319,8 @@ function cloneVaultSettings(settings: VaultSettings): VaultSettings {
286319
dailyNotes: {
287320
enabled: settings.dailyNotes.enabled,
288321
directory: settings.dailyNotes.directory
289-
}
322+
},
323+
folderIcons: { ...settings.folderIcons }
290324
}
291325
}
292326

@@ -310,12 +344,22 @@ function normalizeVaultSettings(
310344
dailyNotes: {
311345
enabled: DEFAULT_VAULT_SETTINGS.dailyNotes.enabled,
312346
directory: DEFAULT_DAILY_NOTES_DIRECTORY
313-
}
347+
},
348+
folderIcons: {}
314349
}
315350
}
316351
const candidate = value as {
317352
primaryNotesLocation?: unknown
318353
dailyNotes?: { enabled?: unknown; directory?: unknown } | null
354+
folderIcons?: Record<string, unknown> | null
355+
}
356+
const folderIcons: Record<string, FolderIconId> = {}
357+
if (candidate.folderIcons && typeof candidate.folderIcons === 'object') {
358+
for (const [key, iconId] of Object.entries(candidate.folderIcons)) {
359+
if (!key || typeof iconId !== 'string') continue
360+
if (!VALID_FOLDER_ICON_IDS.has(iconId as FolderIconId)) continue
361+
folderIcons[key] = iconId
362+
}
319363
}
320364
return {
321365
primaryNotesLocation: normalizePrimaryNotesLocation(
@@ -327,8 +371,72 @@ function normalizeVaultSettings(
327371
? candidate.dailyNotes.enabled
328372
: DEFAULT_VAULT_SETTINGS.dailyNotes.enabled,
329373
directory: normalizeDailyNotesDirectory(candidate.dailyNotes?.directory)
374+
},
375+
folderIcons
376+
}
377+
}
378+
379+
function folderIconKey(folder: NoteFolder, subpath: string): string {
380+
return `${folder}:${subpath}`
381+
}
382+
383+
function rewriteFolderIconsForRename(
384+
folderIcons: Record<string, FolderIconId>,
385+
folder: NoteFolder,
386+
oldSubpath: string,
387+
newSubpath: string
388+
): Record<string, FolderIconId> {
389+
const next: Record<string, FolderIconId> = {}
390+
const exactKey = folderIconKey(folder, oldSubpath)
391+
const prefix = `${exactKey}/`
392+
for (const [key, value] of Object.entries(folderIcons)) {
393+
if (key === exactKey) {
394+
next[folderIconKey(folder, newSubpath)] = value
395+
continue
396+
}
397+
if (key.startsWith(prefix)) {
398+
next[folderIconKey(folder, newSubpath) + key.slice(exactKey.length)] = value
399+
continue
400+
}
401+
next[key] = value
402+
}
403+
return next
404+
}
405+
406+
function removeFolderIcons(
407+
folderIcons: Record<string, FolderIconId>,
408+
folder: NoteFolder,
409+
subpath: string
410+
): Record<string, FolderIconId> {
411+
const next: Record<string, FolderIconId> = {}
412+
const exactKey = folderIconKey(folder, subpath)
413+
const prefix = `${exactKey}/`
414+
for (const [key, value] of Object.entries(folderIcons)) {
415+
if (key === exactKey || key.startsWith(prefix)) continue
416+
next[key] = value
417+
}
418+
return next
419+
}
420+
421+
function duplicateFolderIcons(
422+
folderIcons: Record<string, FolderIconId>,
423+
folder: NoteFolder,
424+
sourceSubpath: string,
425+
targetSubpath: string
426+
): Record<string, FolderIconId> {
427+
const next: Record<string, FolderIconId> = { ...folderIcons }
428+
const exactKey = folderIconKey(folder, sourceSubpath)
429+
const prefix = `${exactKey}/`
430+
for (const [key, value] of Object.entries(folderIcons)) {
431+
if (key === exactKey) {
432+
next[folderIconKey(folder, targetSubpath)] = value
433+
continue
434+
}
435+
if (key.startsWith(prefix)) {
436+
next[folderIconKey(folder, targetSubpath) + key.slice(exactKey.length)] = value
330437
}
331438
}
439+
return next
332440
}
333441

334442
async function inferPrimaryNotesLocation(root: string): Promise<PrimaryNotesLocation> {
@@ -1343,6 +1451,17 @@ export async function renameFolder(
13431451
} else {
13441452
await fs.rename(oldAbs, newAbs)
13451453
}
1454+
const settings = await getVaultSettings(root)
1455+
const nextSettings: VaultSettings = {
1456+
...settings,
1457+
folderIcons: rewriteFolderIconsForRename(
1458+
settings.folderIcons,
1459+
topFolder,
1460+
oldClean,
1461+
newClean
1462+
)
1463+
}
1464+
await setVaultSettings(root, nextSettings)
13461465
return newClean
13471466
}
13481467

@@ -1359,6 +1478,12 @@ export async function deleteFolder(
13591478
if (!clean) throw new Error('Cannot delete the top-level folder')
13601479
const abs = resolveSafe(await folderRoot(root, topFolder), clean)
13611480
await fs.rm(abs, { recursive: true, force: true })
1481+
const settings = await getVaultSettings(root)
1482+
const nextSettings: VaultSettings = {
1483+
...settings,
1484+
folderIcons: removeFolderIcons(settings.folderIcons, topFolder, clean)
1485+
}
1486+
await setVaultSettings(root, nextSettings)
13621487
}
13631488

13641489
/**
@@ -1390,7 +1515,14 @@ export async function duplicateFolder(
13901515
}
13911516
const newAbs = path.join(parentAbs, copyName)
13921517
await fs.cp(oldAbs, newAbs, { recursive: true })
1393-
return path.relative(topRoot, newAbs).split(path.sep).join('/')
1518+
const newSubpath = path.relative(topRoot, newAbs).split(path.sep).join('/')
1519+
const settings = await getVaultSettings(root)
1520+
const nextSettings: VaultSettings = {
1521+
...settings,
1522+
folderIcons: duplicateFolderIcons(settings.folderIcons, topFolder, clean, newSubpath)
1523+
}
1524+
await setVaultSettings(root, nextSettings)
1525+
return newSubpath
13941526
}
13951527

13961528
/** Build the absolute on-disk path for a vault folder / subfolder. */

apps/server/internal/vault/types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
type NoteFolder string
1414
type PrimaryNotesLocation string
15+
type FolderIconID string
1516

1617
const (
1718
FolderInbox NoteFolder = "inbox"
@@ -55,8 +56,9 @@ type DailyNotesSettings struct {
5556
}
5657

5758
type VaultSettings struct {
58-
PrimaryNotesLocation PrimaryNotesLocation `json:"primaryNotesLocation"`
59-
DailyNotes DailyNotesSettings `json:"dailyNotes"`
59+
PrimaryNotesLocation PrimaryNotesLocation `json:"primaryNotesLocation"`
60+
DailyNotes DailyNotesSettings `json:"dailyNotes"`
61+
FolderIcons map[string]FolderIconID `json:"folderIcons"`
6062
}
6163

6264
// NoteMeta — vault-relative note metadata. Mirrors shared/ipc.ts NoteMeta.

0 commit comments

Comments
 (0)