From 7fbf0f07efb13a909c7abf1e3fcac503f2beaa92 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sat, 21 Mar 2026 21:52:58 -0400 Subject: [PATCH] Add multi-tag filtering with active tag display, optimize directory browsing performance, and improve mobile editor UI --- backend/main.py | 17 ++++---- docker-compose.yml | 4 ++ frontend/app.js | 90 ++++++++++++++++++++++++++++++++++-------- frontend/index.html | 73 ++++++++++++++++++---------------- frontend/style.css | 96 ++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 218 insertions(+), 62 deletions(-) diff --git a/backend/main.py b/backend/main.py index 19206cf..8992abf 100644 --- a/backend/main.py +++ b/backend/main.py @@ -114,13 +114,16 @@ async def api_browse(vault_name: str, path: str = ""): continue rel = str(entry.relative_to(vault_root)).replace("\\", "/") if entry.is_dir(): - # Count all supported files recursively (skip hidden dirs) - file_count = sum( - 1 for f in entry.rglob("*") - if f.is_file() - and not any(p.startswith(".") for p in f.relative_to(entry).parts) - and (f.suffix.lower() in SUPPORTED_EXTENSIONS or f.name.lower() in ("dockerfile", "makefile")) - ) + # Count only direct children (files and subdirs) for performance + try: + file_count = sum( + 1 for child in entry.iterdir() + if not child.name.startswith(".") + and (child.is_file() and (child.suffix.lower() in SUPPORTED_EXTENSIONS or child.name.lower() in ("dockerfile", "makefile")) + or child.is_dir()) + ) + except PermissionError: + file_count = 0 items.append({ "name": entry.name, "path": rel, diff --git a/docker-compose.yml b/docker-compose.yml index 6f20e03..aaf86fe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: - /NFS/OBSIDIAN_DOC/Obsidian_MAIN:/vaults/Obsidian_MAIN:ro - /NFS/OBSIDIAN_DOC/Obsidian_WORKOUT:/vaults/Obsidian_WORKOUT:ro - /NFS/OBSIDIAN_DOC/SessionsManager:/vaults/SessionsManager:ro + - /home/bruno:/vaults/bruno:ro environment: - VAULT_1_NAME=Recettes - VAULT_1_PATH=/vaults/Obsidian-RECETTES @@ -24,3 +25,6 @@ services: - VAULT_4_PATH=/vaults/Obsidian_WORKOUT - VAULT_5_NAME=Sessions - VAULT_5_PATH=/vaults/SessionsManager + - VAULT_6_NAME=Bruno + - VAULT_6_PATH=/vaults/bruno + diff --git a/frontend/app.js b/frontend/app.js index e0b76da..a6830a5 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -13,6 +13,7 @@ let cachedRawSource = null; let allVaults = []; let selectedContextVault = "all"; + let selectedTags = []; let editorView = null; let editorVault = null; let editorPath = null; @@ -289,6 +290,8 @@ const data = await api(url); container.innerHTML = ""; + const fragment = document.createDocumentFragment(); + data.items.forEach((item) => { if (item.type === "directory") { const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [ @@ -297,10 +300,10 @@ document.createTextNode(` ${item.name} `), smallBadge(item.children_count), ]); - container.appendChild(dirItem); + fragment.appendChild(dirItem); const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` }); - container.appendChild(subContainer); + fragment.appendChild(subContainer); dirItem.addEventListener("click", async () => { if (subContainer.classList.contains("collapsed")) { @@ -329,10 +332,11 @@ openFile(vaultName, item.path); closeMobileSidebar(); }); - container.appendChild(fileItem); + fragment.appendChild(fileItem); } }); + container.appendChild(fragment); safeCreateIcons(); } @@ -422,11 +426,54 @@ }); } - function searchByTag(tag) { + function addTagFilter(tag) { + if (!selectedTags.includes(tag)) { + selectedTags.push(tag); + renderActiveTags(); + performTagSearch(); + } + } + + function removeTagFilter(tag) { + selectedTags = selectedTags.filter(t => t !== tag); + renderActiveTags(); + if (selectedTags.length > 0) { + performTagSearch(); + } else { + const input = document.getElementById("search-input"); + if (!input.value.trim()) { + showWelcome(); + } + } + } + + function renderActiveTags() { + const container = document.getElementById("active-tags"); + if (!container) return; + + container.innerHTML = ""; + + selectedTags.forEach(tag => { + const tagEl = el("div", { class: "active-tag" }, [ + document.createTextNode(`#${tag}`), + el("span", { class: "remove-icon" }, [icon("x", 14)]) + ]); + tagEl.addEventListener("click", () => removeTagFilter(tag)); + container.appendChild(tagEl); + }); + + safeCreateIcons(); + } + + function performTagSearch() { const input = document.getElementById("search-input"); - input.value = ""; + const query = input.value.trim(); const vault = document.getElementById("vault-filter").value; - performSearch("", vault, tag); + performSearch(query, vault, selectedTags.join(",")); + } + + function searchByTag(tag) { + addTagFilter(tag); } // --------------------------------------------------------------------------- @@ -608,8 +655,9 @@ searchTimeout = setTimeout(() => { const q = input.value.trim(); const vault = document.getElementById("vault-filter").value; - if (q.length > 0) { - performSearch(q, vault, null); + const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null; + if (q.length > 0 || tagFilter) { + performSearch(q, vault, tagFilter); } else { showWelcome(); } @@ -632,10 +680,14 @@ area.innerHTML = ""; const header = el("div", { class: "search-results-header" }); - if (query) { + if (query && tagFilter) { + const tags = tagFilter.split(',').map(t => `#${t}`).join(', '); + header.textContent = `${data.count} résultat(s) pour "${query}" avec les tags ${tags}`; + } else if (query) { header.textContent = `${data.count} résultat(s) pour "${query}"`; } else if (tagFilter) { - header.textContent = `${data.count} fichier(s) avec le tag #${tagFilter}`; + const tags = tagFilter.split(',').map(t => `#${t}`).join(', '); + header.textContent = `${data.count} fichier(s) avec les tags ${tags}`; } area.appendChild(header); @@ -657,7 +709,12 @@ if (r.tags && r.tags.length > 0) { const tagsDiv = el("div", { class: "search-result-tags" }); r.tags.forEach((tag) => { - tagsDiv.appendChild(el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)])); + const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); + tagEl.addEventListener("click", (e) => { + e.stopPropagation(); + addTagFilter(tag); + }); + tagsDiv.appendChild(tagEl); }); item.appendChild(tagsDiv); } @@ -921,11 +978,11 @@ const content = editorView ? editorView.state.doc.toString() : fallbackEditorEl.value; const saveBtn = document.getElementById("editor-save"); - const originalText = saveBtn.textContent; + const originalHTML = saveBtn.innerHTML; try { saveBtn.disabled = true; - saveBtn.innerHTML = ' Sauvegarde...'; + saveBtn.innerHTML = 'Sauvegarde...'; safeCreateIcons(); const response = await fetch( @@ -933,7 +990,7 @@ { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content: content }), + body: JSON.stringify({ content }), } ); @@ -942,12 +999,11 @@ throw new Error(error.detail || "Erreur de sauvegarde"); } - saveBtn.innerHTML = ' Sauvegardé !'; + saveBtn.innerHTML = 'Sauvegardé !'; safeCreateIcons(); setTimeout(() => { closeEditor(); - // Reload the file if it's currently open if (currentVault === editorVault && currentPath === editorPath) { openFile(currentVault, currentPath); } @@ -955,7 +1011,7 @@ } catch (err) { console.error("Save error:", err); alert(`Erreur: ${err.message}`); - saveBtn.innerHTML = originalText; + saveBtn.innerHTML = originalHTML; saveBtn.disabled = false; safeCreateIcons(); } diff --git a/frontend/index.html b/frontend/index.html index 3a18485..7fb2d17 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -64,38 +64,45 @@
- - - - -
- - -
- -
- -
- - - + +
+
+ + +
+
+
+ +
+
+ +
+ + +
@@ -152,11 +159,11 @@
diff --git a/frontend/style.css b/frontend/style.css index 430273a..421e279 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -84,6 +84,7 @@ a:hover { .header { display: flex; align-items: center; + justify-content: space-between; gap: 12px; padding: 10px 20px; background: var(--bg-secondary); @@ -93,6 +94,28 @@ a:hover { transition: background 200ms ease; } +.header-left { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.header-center { + display: flex; + justify-content: center; + flex: 2; + max-width: 600px; +} + +.header-right { + display: flex; + justify-content: flex-end; + flex: 1; + min-width: 0; +} + .hamburger-btn { display: none; background: none; @@ -114,7 +137,6 @@ a:hover { font-size: 1.15rem; color: var(--accent); white-space: nowrap; - margin-right: auto; display: flex; align-items: center; gap: 8px; @@ -128,7 +150,7 @@ a:hover { } .search-wrapper { - flex: 1; + width: 100%; max-width: 520px; position: relative; } @@ -157,6 +179,38 @@ a:hover { pointer-events: none; } +.active-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; + min-height: 0; +} + +.active-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + background: var(--tag-bg); + color: var(--tag-text); + border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + cursor: pointer; + transition: opacity 150ms ease; +} +.active-tag:hover { + opacity: 0.7; +} +.active-tag .remove-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; +} + .header-menu { position: relative; } @@ -569,8 +623,15 @@ a:hover { color: var(--tag-text); border-radius: 4px; font-family: 'JetBrains Mono', monospace; - font-size: 0.78rem; + font-size: 0.75rem; + margin-right: 6px; + margin-bottom: 4px; cursor: pointer; + transition: opacity 150ms ease, transform 100ms ease; +} +.file-tag:hover { + opacity: 0.8; + transform: translateY(-1px); } .file-actions { margin-top: 8px; @@ -980,15 +1041,27 @@ a:hover { .editor-header { padding: 10px 12px; + position: sticky; + top: 0; + z-index: 10; } .editor-title { font-size: 0.85rem; } + .editor-actions { + gap: 6px; + } + .editor-btn { font-size: 0.75rem; - padding: 5px 10px; + padding: 8px; + min-width: 36px; + } + + .editor-btn-text { + display: none; } .editor-body { @@ -1044,6 +1117,20 @@ body.resizing-v { padding: 8px 10px; } + .header-left { + flex: 0 0 auto; + gap: 8px; + } + + .header-center { + flex: 1; + max-width: none; + } + + .header-right { + flex: 0 0 auto; + } + .header-logo { font-size: 0.95rem; gap: 6px; @@ -1051,7 +1138,6 @@ body.resizing-v { .search-wrapper { max-width: none; - flex: 1; } .search-wrapper input {