Des tonnes de modifications notamment VIM / Couleurs / typos

This commit is contained in:
2025-11-11 15:41:51 +01:00
parent 439880b08f
commit 6face7a02f
59 changed files with 7857 additions and 960 deletions

View File

@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
@ -67,6 +68,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleMoveFile(w, r)
return
}
if path == "/api/files/delete-multiple" {
h.handleDeleteMultiple(w, r)
return
}
if path == "/api/notes/new-auto" {
h.handleNewNoteAuto(w, r)
return
@ -83,6 +88,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleHome(w, r)
return
}
if path == "/api/about" {
h.handleAbout(w, r)
return
}
if strings.HasPrefix(path, "/api/daily") {
h.handleDaily(w, r)
return
}
if strings.HasPrefix(path, "/api/notes/") {
h.handleNotes(w, r)
return
@ -91,6 +104,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleFileTree(w, r)
return
}
if strings.HasPrefix(path, "/api/favorites") {
h.handleFavorites(w, r)
return
}
http.NotFound(w, r)
}
@ -247,7 +264,7 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
Content string
IsHome bool
}{
Filename: "🏠 Accueil - Index des notes",
Filename: "🏠 Accueil - Index",
Content: content,
IsHome: true,
}
@ -259,12 +276,26 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
}
}
func (h *Handler) handleAbout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "methode non supportee", http.StatusMethodNotAllowed)
return
}
// Utiliser le template about.html pour afficher la page À propos
err := h.templates.ExecuteTemplate(w, "about.html", nil)
if err != nil {
h.logger.Printf("erreur d execution du template about: %v", err)
http.Error(w, "erreur interne", http.StatusInternalServerError)
}
}
// generateHomeMarkdown génère le contenu Markdown de la page d'accueil
func (h *Handler) generateHomeMarkdown() string {
var sb strings.Builder
// En-tête
sb.WriteString("# 📚 Index des Notes\n\n")
sb.WriteString("# 📚 Index\n\n")
sb.WriteString("_Mise à jour automatique • " + time.Now().Format("02/01/2006 à 15:04") + "_\n\n")
// Construire l'arborescence
@ -281,12 +312,11 @@ func (h *Handler) generateHomeMarkdown() string {
// Section des tags (en premier)
h.generateTagsSection(&sb)
// Statistiques
sb.WriteString(fmt.Sprintf("**%d note(s) au total**\n\n", noteCount))
sb.WriteString("---\n\n")
// Section des favoris (après les tags)
h.generateFavoritesSection(&sb)
// Titre de l'arborescence
sb.WriteString("## 📂 Toutes les notes\n\n")
// Titre de l'arborescence avec le nombre de notes
sb.WriteString(fmt.Sprintf("## 📂 Toutes les notes (%d)\n\n", noteCount))
// Générer l'arborescence en Markdown
h.generateMarkdownTree(&sb, tree, 0)
@ -317,6 +347,90 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) {
sb.WriteString("</div>\n\n")
}
// generateFavoritesSection génère la section des favoris avec arborescence dépliable
func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
favorites, err := h.loadFavorites()
if err != nil || len(favorites.Items) == 0 {
return
}
sb.WriteString("## ⭐ Favoris\n\n")
sb.WriteString("<div class=\"note-tree favorites-tree\">\n")
for _, fav := range favorites.Items {
safeID := "fav-" + strings.ReplaceAll(strings.ReplaceAll(fav.Path, "/", "-"), "\\", "-")
if fav.IsDir {
// Dossier - avec accordéon
sb.WriteString(fmt.Sprintf(" <div class=\"folder indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", safeID))
sb.WriteString(fmt.Sprintf(" <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", safeID))
sb.WriteString(fmt.Sprintf(" <strong>%s</strong>\n", fav.Title))
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" <div class=\"folder-content\" id=\"folder-%s\">\n", safeID))
// Lister le contenu du dossier
h.generateFavoriteFolderContent(sb, fav.Path, 2)
sb.WriteString(fmt.Sprintf(" </div>\n"))
sb.WriteString(fmt.Sprintf(" </div>\n"))
} else {
// Fichier
sb.WriteString(fmt.Sprintf(" <div class=\"file indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", fav.Path))
sb.WriteString(fmt.Sprintf("📄 %s", fav.Title))
sb.WriteString("</a>\n")
sb.WriteString(fmt.Sprintf(" </div>\n"))
}
}
sb.WriteString("</div>\n\n")
}
// generateFavoriteFolderContent génère le contenu d'un dossier favori
func (h *Handler) generateFavoriteFolderContent(sb *strings.Builder, folderPath string, depth int) {
// Construire le chemin absolu
absPath := filepath.Join(h.notesDir, folderPath)
entries, err := os.ReadDir(absPath)
if err != nil {
return
}
indent := strings.Repeat(" ", depth)
indentClass := fmt.Sprintf("indent-level-%d", depth)
for _, entry := range entries {
name := entry.Name()
relativePath := filepath.Join(folderPath, name)
safeID := "fav-" + strings.ReplaceAll(strings.ReplaceAll(relativePath, "/", "-"), "\\", "-")
if entry.IsDir() {
// Sous-dossier
sb.WriteString(fmt.Sprintf("%s<div class=\"folder %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-header\" onclick=\"toggleFolder('%s')\">\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <span class=\"folder-icon\" id=\"icon-%s\">📁</span>\n", indent, safeID))
sb.WriteString(fmt.Sprintf("%s <strong>%s</strong>\n", indent, name))
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s <div class=\"folder-content\" id=\"folder-%s\">\n", indent, safeID))
// Récursion pour les sous-dossiers
h.generateFavoriteFolderContent(sb, relativePath, depth+1)
sb.WriteString(fmt.Sprintf("%s </div>\n", indent))
sb.WriteString(fmt.Sprintf("%s</div>\n", indent))
} else if strings.HasSuffix(name, ".md") {
// Fichier markdown
displayName := strings.TrimSuffix(name, ".md")
sb.WriteString(fmt.Sprintf("%s<div class=\"file %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", indent, relativePath))
sb.WriteString(fmt.Sprintf("📄 %s", displayName))
sb.WriteString("</a>\n")
sb.WriteString(fmt.Sprintf("%s</div>\n", indent))
}
}
}
// countNotes compte le nombre de fichiers .md dans l'arborescence
func (h *Handler) countNotes(node *TreeNode) int {
count := 0
@ -534,6 +648,12 @@ func (h *Handler) handleDeleteNote(w http.ResponseWriter, r *http.Request, filen
return
}
// Nettoyer les dossiers vides parents
parentDir := filepath.Dir(filename)
if parentDir != "." && parentDir != "" {
h.removeEmptyDirRecursive(parentDir)
}
// Re-indexation en arriere-plan
go func() {
if err := h.idx.Load(h.notesDir); err != nil {
@ -843,3 +963,175 @@ func (h *Handler) handleMoveFile(w http.ResponseWriter, r *http.Request) {
h.renderFileTreeOOB(w)
io.WriteString(w, fmt.Sprintf("Fichier déplacé de '%s' vers '%s'", sourcePath, destPath))
}
// handleDeleteMultiple supprime plusieurs fichiers/dossiers en une seule opération
func (h *Handler) handleDeleteMultiple(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.Header().Set("Allow", "DELETE")
http.Error(w, "methode non supportee", http.StatusMethodNotAllowed)
return
}
// For DELETE requests, ParseForm does not read the body. We need to do it manually.
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "lecture du corps de la requete impossible", http.StatusBadRequest)
return
}
defer r.Body.Close()
q, err := url.ParseQuery(string(body))
if err != nil {
http.Error(w, "parsing du corps de la requete impossible", http.StatusBadRequest)
return
}
// Récupérer tous les chemins depuis le formulaire (format: paths[]=path1&paths[]=path2)
paths := q["paths[]"]
if len(paths) == 0 {
http.Error(w, "aucun fichier a supprimer", http.StatusBadRequest)
return
}
deleted := make([]string, 0)
errors := make(map[string]string)
affectedDirs := make(map[string]bool) // Pour suivre les dossiers parents affectés
for _, path := range paths {
// Sécurité : nettoyer le chemin
cleanPath := filepath.Clean(path)
if strings.HasPrefix(cleanPath, "..") || filepath.IsAbs(cleanPath) {
errors[path] = "chemin invalide"
continue
}
fullPath := filepath.Join(h.notesDir, cleanPath)
// Vérifier si le fichier/dossier existe
info, err := os.Stat(fullPath)
if os.IsNotExist(err) {
errors[path] = "fichier introuvable"
continue
}
// Supprimer (récursivement si c'est un dossier)
if info.IsDir() {
err = os.RemoveAll(fullPath)
} else {
err = os.Remove(fullPath)
// Marquer le dossier parent pour nettoyage
parentDir := filepath.Dir(cleanPath)
if parentDir != "." && parentDir != "" {
affectedDirs[parentDir] = true
}
}
if err != nil {
h.logger.Printf("erreur de suppression de %s: %v", path, err)
errors[path] = "suppression impossible"
continue
}
deleted = append(deleted, path)
h.logger.Printf("element supprime: %s", path)
}
// Nettoyer les dossiers vides (remonter l'arborescence)
h.cleanEmptyDirs(affectedDirs)
// Re-indexer en arrière-plan
go func() {
if err := h.idx.Load(h.notesDir); err != nil {
h.logger.Printf("echec de la reindexation post-suppression multiple: %v", err)
}
}()
// Rafraîchir l'arborescence
h.renderFileTreeOOB(w)
// Créer le message de réponse
var message strings.Builder
if len(deleted) > 0 {
message.WriteString(fmt.Sprintf("<p><strong>%d élément(s) supprimé(s) :</strong></p><ul>", len(deleted)))
for _, p := range deleted {
message.WriteString(fmt.Sprintf("<li>%s</li>", p))
}
message.WriteString("</ul>")
}
if len(errors) > 0 {
message.WriteString(fmt.Sprintf("<p><strong>%d erreur(s) :</strong></p><ul>", len(errors)))
for p, e := range errors {
message.WriteString(fmt.Sprintf("<li>%s: %s</li>", p, e))
}
message.WriteString("</ul>")
}
io.WriteString(w, message.String())
}
// cleanEmptyDirs supprime les dossiers vides en remontant l'arborescence
func (h *Handler) cleanEmptyDirs(affectedDirs map[string]bool) {
// Trier les chemins par profondeur décroissante pour commencer par les plus profonds
dirs := make([]string, 0, len(affectedDirs))
for dir := range affectedDirs {
dirs = append(dirs, dir)
}
// Trier par nombre de "/" décroissant (plus profond en premier)
sort.Slice(dirs, func(i, j int) bool {
return strings.Count(dirs[i], string(filepath.Separator)) > strings.Count(dirs[j], string(filepath.Separator))
})
for _, dir := range dirs {
h.removeEmptyDirRecursive(dir)
}
}
// removeEmptyDirRecursive supprime un dossier s'il est vide, puis remonte vers le parent
func (h *Handler) removeEmptyDirRecursive(relPath string) {
if relPath == "" || relPath == "." {
return
}
fullPath := filepath.Join(h.notesDir, relPath)
// Vérifier si le dossier existe
info, err := os.Stat(fullPath)
if err != nil || !info.IsDir() {
return
}
// Lire le contenu du dossier
entries, err := os.ReadDir(fullPath)
if err != nil {
return
}
// Filtrer pour ne compter que les fichiers .md et les dossiers non-cachés
hasContent := false
for _, entry := range entries {
// Ignorer les fichiers cachés
if strings.HasPrefix(entry.Name(), ".") {
continue
}
// Si c'est un .md ou un dossier, le dossier a du contenu
if entry.IsDir() || strings.EqualFold(filepath.Ext(entry.Name()), ".md") {
hasContent = true
break
}
}
// Si le dossier est vide (ne contient que des fichiers cachés ou non-.md)
if !hasContent {
err = os.Remove(fullPath)
if err == nil {
h.logger.Printf("dossier vide supprime: %s", relPath)
// Remonter au parent
parentDir := filepath.Dir(relPath)
if parentDir != "." && parentDir != "" {
h.removeEmptyDirRecursive(parentDir)
}
}
}
}