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:
1 parent
bddbc27a1d
commit
8c29eb35e7
11 files changed
+489
-6
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -71,3 +71,35 @@ func TestTagsFavorites(t *testing.T) {
|
||||
t.Fatalf("après suppression : %+v", tags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderIcon(t *testing.T) {
|
||||
s := newStore(t)
|
||||
s.Write("d/a.excalidraw", []byte(doc), "", true)
|
||||
|
||||
if err := s.SetIcon([]string{"d"}, "server"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, bad := range []struct {
|
||||
path, icon string
|
||||
}{{"d", "Server"}, {"d", "../x"}, {"d/a.excalidraw", "server"}, {"absent", "server"}} {
|
||||
if err := s.SetIcon([]string{bad.path}, bad.icon); err == nil {
|
||||
t.Errorf("SetIcon(%q, %q) aurait dû échouer", bad.path, bad.icon)
|
||||
}
|
||||
}
|
||||
if l, _ := s.List(""); len(l) != 1 || l[0].Icon != "server" {
|
||||
t.Fatalf("List = %+v", l)
|
||||
}
|
||||
|
||||
// L'icône suit le renommage et disparaît quand on rétablit le défaut.
|
||||
s.Move("d", "e")
|
||||
s2, _ := New(s.root)
|
||||
if l, _ := s2.List(""); len(l) != 1 || l[0].Icon != "server" {
|
||||
t.Fatalf("après renommage : %+v", l)
|
||||
}
|
||||
if err := s2.SetIcon([]string{"e"}, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if l, _ := s2.List(""); l[0].Icon != "" {
|
||||
t.Fatalf("après retour au défaut : %+v", l)
|
||||
}
|
||||
}
|
||||
@@ -555,6 +555,24 @@ func (u *Space) EditTags(paths, add, remove []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetIcon choisit l'icône de dossiers (voir Store.SetIcon).
|
||||
func (u *Space) SetIcon(paths []string, icon string) error {
|
||||
groups := map[*Store][]string{}
|
||||
for _, p := range paths {
|
||||
st, rel, err := u.route(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groups[st] = append(groups[st], rel)
|
||||
}
|
||||
for st, rels := range groups {
|
||||
if err := st.SetIcon(rels, icon); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Space) Read(p string) ([]byte, string, error) {
|
||||
st, rel, err := u.route(p)
|
||||
if err != nil {
|
||||
|
||||
@@ -50,6 +50,7 @@ type Entry struct {
|
||||
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
Icon string `json:"icon,omitempty"` // dossier : icône choisie (meta.go)
|
||||
|
||||
// Dossiers virtuels et favoris (spaces.go).
|
||||
Special string `json:"special,omitempty"` // "favoris" ou "perso" : entrée de la racine
|
||||
|
||||
Reference in new issue
Block a user