// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870) import { api } from './auth.js'; import { state } from './state.js'; import { escapeHtml, safeCreateIcons, getFileIcon } from './utils.js'; import { icon, openFile } from './viewer.js'; import { TabManager, showToast } from './ui.js'; // --------------------------------------------------------------------------- // Recent files // --------------------------------------------------------------------------- let _recentRefreshTimer = null; let _recentTimestampTimer = null; let _recentFilesCache = []; // --------------------------------------------------------------------------- // Dashboard Recent Files Widget // --------------------------------------------------------------------------- // ── Dashboard Stats Widget ── const DashboardStatsWidget = { async load() { const grid = document.getElementById("dashboard-stats-grid"); if (!grid) return; grid.innerHTML = '
Chargement...
'; try { const data = await api("/api/dashboard"); this.render(data); } catch (err) { grid.innerHTML = `
Erreur: ${escapeHtml(err.message)}
`; } }, render(data) { const grid = document.getElementById("dashboard-stats-grid"); if (!grid) return; const fmtSize = (bytes) => bytes < 1024 ? `${bytes} o` : bytes < 1048576 ? `${(bytes/1024).toFixed(1)} Ko` : bytes < 1073741824 ? `${(bytes/1048576).toFixed(1)} Mo` : `${(bytes/1073741824).toFixed(1)} Go`; const items = [ { icon: "files", label: "Fichiers", value: data.total_files.toLocaleString() }, { icon: "tags", label: "Tags uniques", value: data.total_tags.toLocaleString() }, { icon: "hard-drive", label: "Taille totale", value: fmtSize(data.total_size_bytes) }, { icon: "folder-open", label: "Vaults", value: data.vaults.length.toString() }, ]; grid.innerHTML = items.map(i => `
${i.value} ${i.label}
`).join(""); safeCreateIcons(); } }; // ── Dashboard Shared Widget ── const DashboardSharedWidget = { async load() { const grid = document.getElementById("dashboard-shared-grid"); const empty = document.getElementById("dashboard-shared-empty"); if (!grid) return; try { const shares = await api("/api/shares"); if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; } if (empty) empty.style.display = "none"; grid.innerHTML = shares.map(s => `
${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))} ${escapeHtml(s.vault)}
${s.access_count || 0} vue(s) ${s.expires_at ? `Expire le ${new Date(s.expires_at).toLocaleDateString("fr-FR")}` : ""}
`).join(""); lucide.createIcons(); grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => { e.stopPropagation(); const url = b.dataset.url; try { await navigator.clipboard.writeText(url); } catch { const ta = document.createElement("textarea"); ta.value=url; ta.style.position="fixed"; ta.style.left="-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } showToast("Lien copié !", "success"); })); grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => { e.stopPropagation(); const card = b.closest(".shared-card"); if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path); })); grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => { e.stopPropagation(); await api(`/api/share/${b.dataset.id}`, { method: "DELETE" }); showToast("Partage révoqué", "success"); this.load(); })); grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => { TabManager.openPreview(card.dataset.vault, card.dataset.path); })); } catch (err) { if (empty) empty.style.display = ""; } } }; // ── Dashboard Conflicts Widget ── const DashboardConflictsWidget = { async load() { const container = document.getElementById("dashboard-conflicts-container"); if (!container) return; try { const data = await api("/api/conflicts"); if (data.total === 0) { container.innerHTML = ""; return; } this.render(data.conflicts, container); } catch (err) { container.innerHTML = ""; } }, render(conflicts, container) { container.innerHTML = `

Conflits de synchronisation

${conflicts.length}
${conflicts.map(c => `
${escapeHtml(c.vault)} ${escapeHtml(c.conflict_path.split("/").pop())} Conflit du ${c.conflict_date.replace(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/, "$3/$2/$1 $4:$5")}
`).join("")}
`; lucide.createIcons(); container.querySelectorAll(".keep-local").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_local"))); container.querySelectorAll(".keep-conflict").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_conflict"))); }, async _resolve(d, action) { try { await api("/api/conflicts/resolve", { method: "POST", body: JSON.stringify({ vault: d.vault, conflict_path: d.conflict, original_path: d.original, action }) }); showToast("Conflit résolu", "success"); this.load(); } catch (err) { showToast("Erreur: " + err.message, "error"); } } }; const DashboardRecentWidget = { _cache: [], _currentFilter: "", async load(vaultFilter = "") { const v = vaultFilter || state.selectedContextVault || "all"; this._currentFilter = v; this.showLoading(); let url = "/api/recent?mode=opened"; if (v !== "all") url += `&vault=${encodeURIComponent(v)}`; try { const data = await api(url); this._cache = data.files || []; this.render(); } catch (err) { console.error("Dashboard: Failed to load recent files:", err); this.showError(); } }, async toggleBookmark(vault, path, title, card) { try { const data = await api("/api/bookmarks/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vault, path, title }), }); // Refresh both widgets to keep sync DashboardBookmarkWidget.load(); // Update current card icon if it exists if (card) { const btn = card.querySelector(".dashboard-card-bookmark-btn"); if (btn) { btn.classList.toggle("active", data.bookmarked); const icon = btn.querySelector("i"); if (icon) icon.setAttribute("data-lucide", data.bookmarked ? "bookmark" : "bookmark-plus"); safeCreateIcons(); } } // Check if we need to refresh the current list to reflect bookmark status across all cards // To avoid flickering, just update the cache and re-render if needed or do a silent refresh this._cache.forEach(f => { if (f.vault === vault && f.path === path) f.bookmarked = data.bookmarked; }); } catch (err) { console.error("Failed to toggle bookmark:", err); showToast("Erreur lors de l'épinglage", "error"); } }, showLoading() { const grid = document.getElementById("dashboard-recent-grid"); const loading = document.getElementById("dashboard-loading"); const empty = document.getElementById("dashboard-recent-empty"); const count = document.getElementById("dashboard-count"); if (grid) grid.innerHTML = ""; if (loading) loading.classList.add("active"); if (empty) empty.classList.add("hidden"); if (count) count.textContent = ""; }, render() { const grid = document.getElementById("dashboard-recent-grid"); const loading = document.getElementById("dashboard-loading"); const empty = document.getElementById("dashboard-recent-empty"); const count = document.getElementById("dashboard-count"); if (loading) loading.classList.remove("active"); if (!this._cache || this._cache.length === 0) { this.showEmpty(); return; } if (empty) empty.classList.add("hidden"); if (count) count.textContent = `${this._cache.length} fichier${this._cache.length > 1 ? "s" : ""}`; if (!grid) return; grid.innerHTML = ""; this._cache.forEach((f, index) => { const card = this._createCard(f, index); grid.appendChild(card); }); safeCreateIcons(); }, _createCard(file, index) { const card = document.createElement("div"); card.className = "dashboard-card"; card.setAttribute("data-vault", file.vault); card.setAttribute("data-path", file.path); card.style.animationDelay = `${Math.min(index * 50, 400)}ms`; // Header with icon and vault badge const header = document.createElement("div"); header.className = "dashboard-card-header"; const iconContainer = document.createElement("div"); iconContainer.className = "dashboard-card-icon"; const fileIconName = getFileIcon(file.path); try { iconContainer.appendChild(icon(fileIconName, 24)); } catch (e) { console.error("Error creating icon:", fileIconName, e); // Fallback to default file icon iconContainer.appendChild(icon("file", 24)); } const badge = document.createElement("span"); badge.className = "dashboard-vault-badge"; badge.textContent = file.vault; const bookmarkBtn = document.createElement("button"); bookmarkBtn.className = `dashboard-card-bookmark-btn ${file.bookmarked ? "active" : ""}`; bookmarkBtn.title = file.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks"; bookmarkBtn.innerHTML = ``; bookmarkBtn.addEventListener("click", (e) => { e.stopPropagation(); this.toggleBookmark(file.vault, file.path, file.title, card); }); header.appendChild(iconContainer); header.appendChild(badge); header.appendChild(bookmarkBtn); card.appendChild(header); // Title const title = document.createElement("h3"); title.className = "dashboard-card-title"; title.textContent = file.title || file.path.split("/").pop(); title.title = file.title || file.path; card.appendChild(title); // Path (compact) const pathParts = file.path.split("/"); if (pathParts.length > 1) { const path = document.createElement("div"); path.className = "dashboard-card-path"; path.textContent = pathParts.slice(0, -1).join(" / "); path.title = file.path; card.appendChild(path); } // Footer with time and tags const footer = document.createElement("div"); footer.className = "dashboard-card-footer"; const time = document.createElement("span"); time.className = "dashboard-card-time"; time.innerHTML = ` ${file.mtime_human || this._humanizeDelta(file.mtime)}`; footer.appendChild(time); // Tags if (file.tags && file.tags.length > 0) { const tags = document.createElement("div"); tags.className = "dashboard-card-tags"; file.tags.slice(0, 3).forEach((tag) => { const tagEl = document.createElement("span"); tagEl.className = "tag-pill"; tagEl.textContent = tag; tags.appendChild(tagEl); }); footer.appendChild(tags); } card.appendChild(footer); // Click handler card.addEventListener("click", () => { openFile(file.vault, file.path); }); return card; }, showEmpty() { const grid = document.getElementById("dashboard-recent-grid"); const loading = document.getElementById("dashboard-loading"); const empty = document.getElementById("dashboard-recent-empty"); const count = document.getElementById("dashboard-count"); if (grid) grid.innerHTML = ""; if (loading) loading.classList.remove("active"); if (empty) empty.classList.remove("hidden"); if (count) count.textContent = "0 fichiers"; safeCreateIcons(); }, showError() { this.showEmpty(); const empty = document.getElementById("dashboard-recent-empty"); if (empty) { const msg = empty.querySelector("span"); if (msg) msg.textContent = "Erreur de chargement"; } }, _humanizeDelta(mtime) { const delta = Date.now() / 1000 - mtime; if (delta < 60) return "à l'instant"; if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`; if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`; if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`; return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" }); }, populateVaultFilter() { const select = document.getElementById("dashboard-vault-filter"); if (!select) return; // Keep first option "Tous les vaults" while (select.options.length > 1) select.remove(1); if (typeof state.allVaults !== "undefined" && Array.isArray(state.allVaults)) { state.allVaults.forEach((v) => { const opt = document.createElement("option"); opt.value = v.name; opt.textContent = v.name; select.appendChild(opt); }); } syncVaultSelectors(); }, init() { const select = document.getElementById("dashboard-vault-filter"); if (select) { select.addEventListener("change", async () => { await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" }); }); } this.populateVaultFilter(); }, }; // ── Dashboard Bookmarks Widget ── // (moved from app.js 3810-3870; logically belongs with dashboard widgets) const DashboardBookmarkWidget = { _cache: [], _currentFilter: "", async load(vaultFilter = "") { const v = vaultFilter || state.selectedContextVault || "all"; this._currentFilter = v; this.showLoading(); let url = "/api/bookmarks"; if (v !== "all") url += `?vault=${encodeURIComponent(v)}`; try { const data = await api(url); this._cache = data.files || []; this.render(); } catch (err) { console.error("Dashboard: Failed to load bookmarks:", err); this.showEmpty(); } }, showLoading() { const grid = document.getElementById("dashboard-bookmarks-grid"); const empty = document.getElementById("dashboard-bookmarks-empty"); const section = document.getElementById("dashboard-bookmarks-section"); if (grid) grid.innerHTML = ""; if (empty) empty.classList.add("hidden"); }, render() { const grid = document.getElementById("dashboard-bookmarks-grid"); const empty = document.getElementById("dashboard-bookmarks-empty"); const section = document.getElementById("dashboard-bookmarks-section"); if (!this._cache || this._cache.length === 0) { if (grid) grid.innerHTML = ""; if (empty) empty.classList.remove("hidden"); return; } if (empty) empty.classList.add("hidden"); if (!grid) return; grid.innerHTML = ""; this._cache.forEach((f, idx) => { const card = DashboardRecentWidget._createCard(f, idx); grid.appendChild(card); }); safeCreateIcons(); }, showEmpty() { const grid = document.getElementById("dashboard-bookmarks-grid"); const empty = document.getElementById("dashboard-bookmarks-empty"); if (grid) grid.innerHTML = ""; if (empty) empty.classList.remove("hidden"); } }; export { DashboardRecentWidget, DashboardStatsWidget, DashboardBookmarkWidget, DashboardSharedWidget, DashboardConflictsWidget };