Tags et favoris, filtres dans l'explorateur

- DATA_DIR/.meta.json : tags et favoris par chemin, suivis aux renommages,
  déplacements, copies (tags seuls) et suppressions.
- API : GET /api/tags, POST /api/meta/favorite, POST /api/meta/tags ; Entry
  porte tags et favorite ; /api/search accepte tag= et fav=true (q facultatif).
- Tags comparés sans casse ni accents, écriture existante conservée ; ils
  participent à la recherche texte.
- Explorateur : ligne de filtres (Favoris, tags avec leur nombre), étoile et
  tags sur les éléments, menu Favori / Tags…, actions sur la sélection,
  dialogue Tags (ajout, retrait, suggestions).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6J7vKwXx7uopxKhWfFJpm
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-07 18:06:45 +02:00
1 parent c78df092c1
commit c1ea1e84a7
13 files changed
+880 -33

No files matched your search

+27 -8
View File
@@ -139,14 +139,23 @@ func snippet(d indexDoc, i, n int) string {
return pre + string(runes[lo:hi]) + post
}
// Search renvoie les entrées dont le nom ou, pour un schéma, le contenu
// contient tous les mots de q (casse et accents ignorés). Les correspondances
// sur le nom viennent en premier.
func (s *Store) Search(q string, limit int) ([]Hit, error) {
// Filter restreint une recherche aux éléments portant un tag ou favoris.
type Filter struct {
Tag string
Favorite bool
}
// Search renvoie les entrées dont le nom, les tags ou, pour un schéma, le
// contenu contiennent tous les mots de q (casse et accents ignorés), et qui
// passent le filtre. Les correspondances sur le nom viennent en premier.
// q peut être vide si un filtre est donné.
func (s *Store) Search(q string, f Filter, limit int) ([]Hit, error) {
terms := strings.Fields(fold(q))
if len(terms) == 0 {
tag := fold(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(f.Tag), "#")))
if len(terms) == 0 && tag == "" && !f.Favorite {
return []Hit{}, nil
}
meta := s.metaSnapshot()
s.index.mu.Lock()
defer s.index.mu.Unlock()
if s.index.docs == nil {
@@ -176,13 +185,23 @@ func (s *Store) Search(q string, limit int) ([]Hit, error) {
return nil
}
rel = filepath.ToSlash(rel)
name := fold(d.Name())
hit := Hit{Entry: toEntry(rel, fi)}
decorate(&hit.Entry, meta)
if f.Favorite && !hit.Favorite {
return nil
}
tags := fold(strings.Join(hit.Tags, "\n"))
if tag != "" && !strings.Contains("\n"+tags+"\n", "\n"+tag+"\n") {
return nil
}
name := fold(d.Name()) + "\n" + tags
var doc indexDoc
if !d.IsDir() {
seen[rel] = true
doc = s.content(rel, p, fi)
if len(terms) > 0 {
doc = s.content(rel, p, fi)
}
}
hit := Hit{Entry: toEntry(rel, fi)}
inName := true
for _, t := range terms {
if strings.Contains(name, t) {