Explorateur : icône personnalisable par dossier

Menu « … » d'un dossier > « Icône… » : grille de 27 icônes Tabler
(projet, serveurs, stockage, réseau, sécurité, documents, archives…)
plus « Par défaut ». L'icône est enregistrée dans .meta.json (champ
icon), partagée par tous, et suit renommages et déplacements.
Nouvelle route POST /api/meta/icon {"paths":[...],"icon":"server"}.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011bGUm1KLMvT6YPbJxZgwkj
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-15 09:14:39 +02:00
1 parent bddbc27a1d
commit 8c29eb35e7
11 files changed
+489 -6

No files matched your search

+36 -4
View File
@@ -7,14 +7,16 @@ import (
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"unicode/utf8"
)
// Tags et favoris des fichiers et dossiers, dans .meta.json à la racine :
// {"items":{"a/b.excalidraw":{"tags":["NSX"],"favorite":true}}}. Ils suivent
// les renommages, déplacements et suppressions.
// Tags et favoris des fichiers et dossiers, et icône des dossiers, dans
// .meta.json à la racine :
// {"items":{"a/b.excalidraw":{"tags":["NSX"],"favorite":true},"a":{"icon":"server"}}}.
// Ils suivent les renommages, déplacements et suppressions.
const metaFile = ".meta.json"
@@ -28,9 +30,14 @@ const (
type itemMeta struct {
Tags []string `json:"tags,omitempty"`
Favorite bool `json:"favorite,omitempty"`
Icon string `json:"icon,omitempty"` // dossiers : nom d'icône du frontend
}
func (m itemMeta) empty() bool { return len(m.Tags) == 0 && !m.Favorite }
func (m itemMeta) empty() bool { return len(m.Tags) == 0 && !m.Favorite && m.Icon == "" }
// iconName : nom d'icône accepté (le frontend affiche le dossier classique
// pour un nom qu'il ne connaît pas).
var iconName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,31}$`)
// TagCount est un tag utilisé et son nombre d'éléments.
type TagCount struct {
@@ -94,6 +101,9 @@ func (s *Store) metaSnapshot() map[string]itemMeta {
func decorate(e *Entry, meta map[string]itemMeta) {
if m, ok := meta[e.Path]; ok {
e.Tags, e.Favorite = m.Tags, m.Favorite
if e.IsDir {
e.Icon = m.Icon
}
}
}
@@ -149,6 +159,28 @@ func (s *Store) SetFavorite(paths []string, favorite bool) error {
})
}
// SetIcon choisit l'icône de dossiers ; icon = "" rétablit l'icône par défaut.
func (s *Store) SetIcon(paths []string, icon string) error {
if icon != "" && !iconName.MatchString(icon) {
return fmt.Errorf("%w : icône %q", ErrInvalidPath, icon)
}
for _, p := range paths {
_, abs, err := s.resolve(p)
if err != nil {
return err
}
if fi, err := os.Lstat(abs); err != nil {
return mapErr(err)
} else if !fi.IsDir() {
return fmt.Errorf("%w : %q n'est pas un dossier", ErrInvalidPath, p)
}
}
return s.editMeta(paths, func(m *itemMeta) error {
m.Icon = icon
return nil
})
}
// EditTags ajoute et retire des tags sur des éléments. Les tags sont comparés
// sans casse ni accents ; un tag déjà utilisé garde son écriture d'origine.
func (s *Store) EditTags(paths, add, remove []string) error {