frontend/js/ structure: state.js (55 lines) — Shared mutable state, constants utils.js (510 lines) — EXT_ICONS, getFileIcon, escapeHtml, safeCreateIcons auth.js (547 lines) — api(), AuthManager, initLoginForm, AdminPanel search.js (1106 lines)— SearchHistory, QueryParser, Autocomplete, performSearch sidebar.js (1091 lines)— Vault tree, sidebar filter, TagFilterService, loadTags viewer.js (1554 lines)— openFile, Outline, ScrollSpy, Frontmatter, Editor ui.js (2250 lines)— Theme, Toast, Sidebar, Dropdowns, Tabs, ContextMenu dashboard.js (461 lines) — Dashboard widgets (Recent, Stats, Bookmarks) config.js (999 lines) — Config panel, Hidden files, About, Sidebar tabs sync.js (436 lines) — SSE/IndexUpdateManager, PWA registration graph.js (401 lines) — GraphViewManager (force-directed canvas graph) legacy.js (550 lines) — Remaining bridge functions (goHome, showWelcome, initSearch) app.js (80 lines) — Thin orchestrator: imports all modules, calls init() index.html: switched from <script src="app.js"> to <script type="module" src="js/app.js"> Original app.js preserved for backward compatibility. All 14 modules pass node --check syntax validation.
462 lines
18 KiB
JavaScript
462 lines
18 KiB
JavaScript
// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
|
|
import { selectedContextVault, allVaults } from './state.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 = '<div class="dashboard-stats-loading">Chargement...</div>';
|
|
try {
|
|
const data = await api("/api/dashboard");
|
|
this.render(data);
|
|
} catch (err) {
|
|
grid.innerHTML = `<div class="dashboard-recent-empty">Erreur: ${escapeHtml(err.message)}</div>`;
|
|
}
|
|
},
|
|
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 => `
|
|
<div class="stat-card">
|
|
<i data-lucide="${i.icon}" class="stat-icon"></i>
|
|
<span class="stat-value">${i.value}</span>
|
|
<span class="stat-label">${i.label}</span>
|
|
</div>
|
|
`).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 => `
|
|
<div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}">
|
|
<div class="shared-card-header">
|
|
<i data-lucide="file-text" style="width:14px;height:14px"></i>
|
|
<span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))}</span>
|
|
<span class="shared-card-vault">${escapeHtml(s.vault)}</span>
|
|
</div>
|
|
<div class="shared-card-meta">
|
|
<span>${s.access_count || 0} vue(s)</span>
|
|
${s.expires_at ? `<span>Expire le ${new Date(s.expires_at).toLocaleDateString("fr-FR")}</span>` : ""}
|
|
</div>
|
|
<div class="shared-card-actions">
|
|
<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">📋 Copier</button>
|
|
<button class="shared-open-btn">📂 Ouvrir</button>
|
|
<button class="shared-revoke-btn" data-id="${s.id}">🗑</button>
|
|
</div>
|
|
</div>
|
|
`).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 = `
|
|
<div class="dashboard-section">
|
|
<div class="dashboard-header">
|
|
<div class="dashboard-title-row">
|
|
<i data-lucide="alert-triangle" class="dashboard-icon" style="color:var(--accent-orange)"></i>
|
|
<h2>Conflits de synchronisation</h2>
|
|
<span class="dashboard-badge" style="background:var(--accent-orange)">${conflicts.length}</span>
|
|
</div>
|
|
</div>
|
|
<div class="dashboard-conflicts-grid">
|
|
${conflicts.map(c => `
|
|
<div class="conflict-card">
|
|
<div class="conflict-info">
|
|
<span class="conflict-vault">${escapeHtml(c.vault)}</span>
|
|
<span class="conflict-name">${escapeHtml(c.conflict_path.split("/").pop())}</span>
|
|
<span class="conflict-date">Conflit du ${c.conflict_date.replace(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/, "$3/$2/$1 $4:$5")}</span>
|
|
</div>
|
|
<div class="conflict-actions">
|
|
<button class="conflict-btn keep-local" data-vault="${escapeHtml(c.vault)}" data-conflict="${escapeHtml(c.conflict_path)}" data-original="${escapeHtml(c.original_path)}">Garder l'original</button>
|
|
<button class="conflict-btn keep-conflict" data-vault="${escapeHtml(c.vault)}" data-conflict="${escapeHtml(c.conflict_path)}" data-original="${escapeHtml(c.original_path)}">Garder le conflit</button>
|
|
</div>
|
|
</div>
|
|
`).join("")}
|
|
</div>
|
|
</div>`;
|
|
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 || 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 = `<i data-lucide="${file.bookmarked ? "bookmark" : "bookmark-plus"}" style="width:14px;height:14px"></i>`;
|
|
|
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> ${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 allVaults !== "undefined" && Array.isArray(allVaults)) {
|
|
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 || 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 };
|