refactor: state.js → mutable object to fix 'assignment to constant' errors
ES module imports are read-only live bindings — can't reassign
imported let/const variables. Replace individual 'export let' with
single 'export const state = {...}' mutable object.
All modules updated: import { state } from './state.js'
All state access changed to state.xxx pattern.
Fixes cascade of 'Assignment to constant variable' errors.
This commit is contained in:
+462
-461
@@ -1,461 +1,462 @@
|
||||
// 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, DashboardSharedWidget };
|
||||
1|// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
|
||||
import { state } from './state.js';
|
||||
3|
|
||||
4|// ---------------------------------------------------------------------------
|
||||
5|// Recent files
|
||||
6|// ---------------------------------------------------------------------------
|
||||
7|let _recentRefreshTimer = null;
|
||||
8|let _recentTimestampTimer = null;
|
||||
9|let _recentFilesCache = [];
|
||||
10|
|
||||
11|// ---------------------------------------------------------------------------
|
||||
12|// Dashboard Recent Files Widget
|
||||
13|// ---------------------------------------------------------------------------
|
||||
14|// ── Dashboard Stats Widget ──
|
||||
15|const DashboardStatsWidget = {
|
||||
16| async load() {
|
||||
17| const grid = document.getElementById("dashboard-stats-grid");
|
||||
18| if (!grid) return;
|
||||
19| grid.innerHTML = '<div class="dashboard-stats-loading">Chargement...</div>';
|
||||
20| try {
|
||||
21| const data = await api("/api/dashboard");
|
||||
22| this.render(data);
|
||||
23| } catch (err) {
|
||||
24| grid.innerHTML = `<div class="dashboard-recent-empty">Erreur: ${escapeHtml(err.message)}</div>`;
|
||||
25| }
|
||||
26| },
|
||||
27| render(data) {
|
||||
28| const grid = document.getElementById("dashboard-stats-grid");
|
||||
29| if (!grid) return;
|
||||
30| 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`;
|
||||
31| const items = [
|
||||
32| { icon: "files", label: "Fichiers", value: data.total_files.toLocaleString() },
|
||||
33| { icon: "tags", label: "Tags uniques", value: data.total_tags.toLocaleString() },
|
||||
34| { icon: "hard-drive", label: "Taille totale", value: fmtSize(data.total_size_bytes) },
|
||||
35| { icon: "folder-open", label: "Vaults", value: data.vaults.length.toString() },
|
||||
36| ];
|
||||
37| grid.innerHTML = items.map(i => `
|
||||
38| <div class="stat-card">
|
||||
39| <i data-lucide="${i.icon}" class="stat-icon"></i>
|
||||
40| <span class="stat-value">${i.value}</span>
|
||||
41| <span class="stat-label">${i.label}</span>
|
||||
42| </div>
|
||||
43| `).join("");
|
||||
44| safeCreateIcons();
|
||||
45| }
|
||||
46|};
|
||||
47|
|
||||
48|// ── Dashboard Shared Widget ──
|
||||
49|const DashboardSharedWidget = {
|
||||
50| async load() {
|
||||
51| const grid = document.getElementById("dashboard-shared-grid");
|
||||
52| const empty = document.getElementById("dashboard-shared-empty");
|
||||
53| if (!grid) return;
|
||||
54| try {
|
||||
55| const shares = await api("/api/shares");
|
||||
56| if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
|
||||
57| if (empty) empty.style.display = "none";
|
||||
58| grid.innerHTML = shares.map(s => `
|
||||
59| <div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}">
|
||||
60| <div class="shared-card-header">
|
||||
61| <i data-lucide="file-text" style="width:14px;height:14px"></i>
|
||||
62| <span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))}</span>
|
||||
63| <span class="shared-card-vault">${escapeHtml(s.vault)}</span>
|
||||
64| </div>
|
||||
65| <div class="shared-card-meta">
|
||||
66| <span>${s.access_count || 0} vue(s)</span>
|
||||
67| ${s.expires_at ? `<span>Expire le ${new Date(s.expires_at).toLocaleDateString("fr-FR")}</span>` : ""}
|
||||
68| </div>
|
||||
69| <div class="shared-card-actions">
|
||||
70| <button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">📋 Copier</button>
|
||||
71| <button class="shared-open-btn">📂 Ouvrir</button>
|
||||
72| <button class="shared-revoke-btn" data-id="${s.id}">🗑</button>
|
||||
73| </div>
|
||||
74| </div>
|
||||
75| `).join("");
|
||||
76| lucide.createIcons();
|
||||
77| grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||
78| e.stopPropagation();
|
||||
79| const url = b.dataset.url;
|
||||
80| 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); }
|
||||
81| showToast("Lien copié !", "success");
|
||||
82| }));
|
||||
83| grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
|
||||
84| e.stopPropagation();
|
||||
85| const card = b.closest(".shared-card");
|
||||
86| if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||
87| }));
|
||||
88| grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||
89| e.stopPropagation();
|
||||
90| await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
|
||||
91| showToast("Partage révoqué", "success");
|
||||
92| this.load();
|
||||
93| }));
|
||||
94| grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
|
||||
95| TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||
96| }));
|
||||
97| } catch (err) { if (empty) empty.style.display = ""; }
|
||||
98| }
|
||||
99|};
|
||||
100|
|
||||
101|// ── Dashboard Conflicts Widget ──
|
||||
102|const DashboardConflictsWidget = {
|
||||
103| async load() {
|
||||
104| const container = document.getElementById("dashboard-conflicts-container");
|
||||
105| if (!container) return;
|
||||
106| try {
|
||||
107| const data = await api("/api/conflicts");
|
||||
108| if (data.total === 0) { container.innerHTML = ""; return; }
|
||||
109| this.render(data.conflicts, container);
|
||||
110| } catch (err) { container.innerHTML = ""; }
|
||||
111| },
|
||||
112| render(conflicts, container) {
|
||||
113| container.innerHTML = `
|
||||
114| <div class="dashboard-section">
|
||||
115| <div class="dashboard-header">
|
||||
116| <div class="dashboard-title-row">
|
||||
117| <i data-lucide="alert-triangle" class="dashboard-icon" style="color:var(--accent-orange)"></i>
|
||||
118| <h2>Conflits de synchronisation</h2>
|
||||
119| <span class="dashboard-badge" style="background:var(--accent-orange)">${conflicts.length}</span>
|
||||
120| </div>
|
||||
121| </div>
|
||||
122| <div class="dashboard-conflicts-grid">
|
||||
123| ${conflicts.map(c => `
|
||||
124| <div class="conflict-card">
|
||||
125| <div class="conflict-info">
|
||||
126| <span class="conflict-vault">${escapeHtml(c.vault)}</span>
|
||||
127| <span class="conflict-name">${escapeHtml(c.conflict_path.split("/").pop())}</span>
|
||||
128| <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>
|
||||
129| </div>
|
||||
130| <div class="conflict-actions">
|
||||
131| <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>
|
||||
132| <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>
|
||||
133| </div>
|
||||
134| </div>
|
||||
135| `).join("")}
|
||||
136| </div>
|
||||
137| </div>`;
|
||||
138| lucide.createIcons();
|
||||
139| container.querySelectorAll(".keep-local").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_local")));
|
||||
140| container.querySelectorAll(".keep-conflict").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_conflict")));
|
||||
141| },
|
||||
142| async _resolve(d, action) {
|
||||
143| try {
|
||||
144| await api("/api/conflicts/resolve", { method: "POST", body: JSON.stringify({ vault: d.vault, conflict_path: d.conflict, original_path: d.original, action }) });
|
||||
145| showToast("Conflit résolu", "success");
|
||||
146| this.load();
|
||||
147| } catch (err) { showToast("Erreur: " + err.message, "error"); }
|
||||
148| }
|
||||
149|};
|
||||
150|
|
||||
151|const DashboardRecentWidget = {
|
||||
152| _cache: [],
|
||||
153| _currentFilter: "",
|
||||
154|
|
||||
155| async load(vaultFilter = "") {
|
||||
156| const v = vaultFilter || selectedContextVault || "all";
|
||||
157| this._currentFilter = v;
|
||||
158| this.showLoading();
|
||||
159|
|
||||
160| let url = "/api/recent?mode=opened";
|
||||
161| if (v !== "all") url += `&vault=${encodeURIComponent(v)}`;
|
||||
162|
|
||||
163| try {
|
||||
164| const data = await api(url);
|
||||
165| this._cache = data.files || [];
|
||||
166| this.render();
|
||||
167| } catch (err) {
|
||||
168| console.error("Dashboard: Failed to load recent files:", err);
|
||||
169| this.showError();
|
||||
170| }
|
||||
171| },
|
||||
172|
|
||||
173| async toggleBookmark(vault, path, title, card) {
|
||||
174| try {
|
||||
175| const data = await api("/api/bookmarks/toggle", {
|
||||
176| method: "POST",
|
||||
177| headers: { "Content-Type": "application/json" },
|
||||
178| body: JSON.stringify({ vault, path, title }),
|
||||
179| });
|
||||
180|
|
||||
181| // Refresh both widgets to keep sync
|
||||
182| DashboardBookmarkWidget.load();
|
||||
183|
|
||||
184| // Update current card icon if it exists
|
||||
185| if (card) {
|
||||
186| const btn = card.querySelector(".dashboard-card-bookmark-btn");
|
||||
187| if (btn) {
|
||||
188| btn.classList.toggle("active", data.bookmarked);
|
||||
189| const icon = btn.querySelector("i");
|
||||
190| if (icon) icon.setAttribute("data-lucide", data.bookmarked ? "bookmark" : "bookmark-plus");
|
||||
191| safeCreateIcons();
|
||||
192| }
|
||||
193| }
|
||||
194|
|
||||
195| // Check if we need to refresh the current list to reflect bookmark status across all cards
|
||||
196| // To avoid flickering, just update the cache and re-render if needed or do a silent refresh
|
||||
197| this._cache.forEach(f => {
|
||||
198| if (f.vault === vault && f.path === path) f.bookmarked = data.bookmarked;
|
||||
199| });
|
||||
200| } catch (err) {
|
||||
201| console.error("Failed to toggle bookmark:", err);
|
||||
202| showToast("Erreur lors de l'épinglage", "error");
|
||||
203| }
|
||||
204| },
|
||||
205|
|
||||
206| showLoading() {
|
||||
207| const grid = document.getElementById("dashboard-recent-grid");
|
||||
208| const loading = document.getElementById("dashboard-loading");
|
||||
209| const empty = document.getElementById("dashboard-recent-empty");
|
||||
210| const count = document.getElementById("dashboard-count");
|
||||
211|
|
||||
212| if (grid) grid.innerHTML = "";
|
||||
213| if (loading) loading.classList.add("active");
|
||||
214| if (empty) empty.classList.add("hidden");
|
||||
215| if (count) count.textContent = "";
|
||||
216| },
|
||||
217|
|
||||
218| render() {
|
||||
219| const grid = document.getElementById("dashboard-recent-grid");
|
||||
220| const loading = document.getElementById("dashboard-loading");
|
||||
221| const empty = document.getElementById("dashboard-recent-empty");
|
||||
222| const count = document.getElementById("dashboard-count");
|
||||
223|
|
||||
224| if (loading) loading.classList.remove("active");
|
||||
225|
|
||||
226| if (!this._cache || this._cache.length === 0) {
|
||||
227| this.showEmpty();
|
||||
228| return;
|
||||
229| }
|
||||
230|
|
||||
231| if (empty) empty.classList.add("hidden");
|
||||
232| if (count) count.textContent = `${this._cache.length} fichier${this._cache.length > 1 ? "s" : ""}`;
|
||||
233|
|
||||
234| if (!grid) return;
|
||||
235| grid.innerHTML = "";
|
||||
236|
|
||||
237| this._cache.forEach((f, index) => {
|
||||
238| const card = this._createCard(f, index);
|
||||
239| grid.appendChild(card);
|
||||
240| });
|
||||
241|
|
||||
242| safeCreateIcons();
|
||||
243| },
|
||||
244|
|
||||
245| _createCard(file, index) {
|
||||
246| const card = document.createElement("div");
|
||||
247| card.className = "dashboard-card";
|
||||
248| card.setAttribute("data-vault", file.vault);
|
||||
249| card.setAttribute("data-path", file.path);
|
||||
250| card.style.animationDelay = `${Math.min(index * 50, 400)}ms`;
|
||||
251|
|
||||
252| // Header with icon and vault badge
|
||||
253| const header = document.createElement("div");
|
||||
254| header.className = "dashboard-card-header";
|
||||
255|
|
||||
256| const iconContainer = document.createElement("div");
|
||||
257| iconContainer.className = "dashboard-card-icon";
|
||||
258| const fileIconName = getFileIcon(file.path);
|
||||
259| try {
|
||||
260| iconContainer.appendChild(icon(fileIconName, 24));
|
||||
261| } catch (e) {
|
||||
262| console.error("Error creating icon:", fileIconName, e);
|
||||
263| // Fallback to default file icon
|
||||
264| iconContainer.appendChild(icon("file", 24));
|
||||
265| }
|
||||
266|
|
||||
267| const badge = document.createElement("span");
|
||||
268| badge.className = "dashboard-vault-badge";
|
||||
269| badge.textContent = file.vault;
|
||||
270|
|
||||
271| const bookmarkBtn = document.createElement("button");
|
||||
272| bookmarkBtn.className = `dashboard-card-bookmark-btn ${file.bookmarked ? "active" : ""}`;
|
||||
273| bookmarkBtn.title = file.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks";
|
||||
274| bookmarkBtn.innerHTML = `<i data-lucide="${file.bookmarked ? "bookmark" : "bookmark-plus"}" style="width:14px;height:14px"></i>`;
|
||||
275|
|
||||
276| bookmarkBtn.addEventListener("click", (e) => {
|
||||
277| e.stopPropagation();
|
||||
278| this.toggleBookmark(file.vault, file.path, file.title, card);
|
||||
279| });
|
||||
280|
|
||||
281| header.appendChild(iconContainer);
|
||||
282| header.appendChild(badge);
|
||||
283| header.appendChild(bookmarkBtn);
|
||||
284| card.appendChild(header);
|
||||
285|
|
||||
286| // Title
|
||||
287| const title = document.createElement("h3");
|
||||
288| title.className = "dashboard-card-title";
|
||||
289| title.textContent = file.title || file.path.split("/").pop();
|
||||
290| title.title = file.title || file.path;
|
||||
291| card.appendChild(title);
|
||||
292|
|
||||
293| // Path (compact)
|
||||
294| const pathParts = file.path.split("/");
|
||||
295| if (pathParts.length > 1) {
|
||||
296| const path = document.createElement("div");
|
||||
297| path.className = "dashboard-card-path";
|
||||
298| path.textContent = pathParts.slice(0, -1).join(" / ");
|
||||
299| path.title = file.path;
|
||||
300| card.appendChild(path);
|
||||
301| }
|
||||
302|
|
||||
303| // Footer with time and tags
|
||||
304| const footer = document.createElement("div");
|
||||
305| footer.className = "dashboard-card-footer";
|
||||
306|
|
||||
307| const time = document.createElement("span");
|
||||
308| time.className = "dashboard-card-time";
|
||||
309| 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)}`;
|
||||
310|
|
||||
311| footer.appendChild(time);
|
||||
312|
|
||||
313| // Tags
|
||||
314| if (file.tags && file.tags.length > 0) {
|
||||
315| const tags = document.createElement("div");
|
||||
316| tags.className = "dashboard-card-tags";
|
||||
317| file.tags.slice(0, 3).forEach((tag) => {
|
||||
318| const tagEl = document.createElement("span");
|
||||
319| tagEl.className = "tag-pill";
|
||||
320| tagEl.textContent = tag;
|
||||
321| tags.appendChild(tagEl);
|
||||
322| });
|
||||
323| footer.appendChild(tags);
|
||||
324| }
|
||||
325|
|
||||
326| card.appendChild(footer);
|
||||
327|
|
||||
328| // Click handler
|
||||
329| card.addEventListener("click", () => {
|
||||
330| openFile(file.vault, file.path);
|
||||
331| });
|
||||
332|
|
||||
333| return card;
|
||||
334| },
|
||||
335|
|
||||
336| showEmpty() {
|
||||
337| const grid = document.getElementById("dashboard-recent-grid");
|
||||
338| const loading = document.getElementById("dashboard-loading");
|
||||
339| const empty = document.getElementById("dashboard-recent-empty");
|
||||
340| const count = document.getElementById("dashboard-count");
|
||||
341|
|
||||
342| if (grid) grid.innerHTML = "";
|
||||
343| if (loading) loading.classList.remove("active");
|
||||
344| if (empty) empty.classList.remove("hidden");
|
||||
345| if (count) count.textContent = "0 fichiers";
|
||||
346| safeCreateIcons();
|
||||
347| },
|
||||
348|
|
||||
349| showError() {
|
||||
350| this.showEmpty();
|
||||
351| const empty = document.getElementById("dashboard-recent-empty");
|
||||
352| if (empty) {
|
||||
353| const msg = empty.querySelector("span");
|
||||
354| if (msg) msg.textContent = "Erreur de chargement";
|
||||
355| }
|
||||
356| },
|
||||
357|
|
||||
358| _humanizeDelta(mtime) {
|
||||
359| const delta = Date.now() / 1000 - mtime;
|
||||
360| if (delta < 60) return "à l'instant";
|
||||
361| if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
|
||||
362| if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
|
||||
363| if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
|
||||
364| return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
|
||||
365| },
|
||||
366|
|
||||
367| populateVaultFilter() {
|
||||
368| const select = document.getElementById("dashboard-vault-filter");
|
||||
369| if (!select) return;
|
||||
370|
|
||||
371| // Keep first option "Tous les vaults"
|
||||
372| while (select.options.length > 1) select.remove(1);
|
||||
373|
|
||||
374| if (typeof allVaults !== "undefined" && Array.isArray(state.allVaults)) {
|
||||
375| state.allVaults.forEach((v) => {
|
||||
376| const opt = document.createElement("option");
|
||||
377| opt.value = v.name;
|
||||
378| opt.textContent = v.name;
|
||||
379| select.appendChild(opt);
|
||||
380| });
|
||||
381| }
|
||||
382| syncVaultSelectors();
|
||||
383| },
|
||||
384|
|
||||
385| init() {
|
||||
386| const select = document.getElementById("dashboard-vault-filter");
|
||||
387| if (select) {
|
||||
388| select.addEventListener("change", async () => {
|
||||
389| await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" });
|
||||
390| });
|
||||
391| }
|
||||
392|
|
||||
393| this.populateVaultFilter();
|
||||
394| },
|
||||
395|};
|
||||
396|
|
||||
397|// ── Dashboard Bookmarks Widget ──
|
||||
398|// (moved from app.js 3810-3870; logically belongs with dashboard widgets)
|
||||
399|const DashboardBookmarkWidget = {
|
||||
400| _cache: [],
|
||||
401| _currentFilter: "",
|
||||
402|
|
||||
403| async load(vaultFilter = "") {
|
||||
404| const v = vaultFilter || selectedContextVault || "all";
|
||||
405| this._currentFilter = v;
|
||||
406| this.showLoading();
|
||||
407|
|
||||
408| let url = "/api/bookmarks";
|
||||
409| if (v !== "all") url += `?vault=${encodeURIComponent(v)}`;
|
||||
410|
|
||||
411| try {
|
||||
412| const data = await api(url);
|
||||
413| this._cache = data.files || [];
|
||||
414| this.render();
|
||||
415| } catch (err) {
|
||||
416| console.error("Dashboard: Failed to load bookmarks:", err);
|
||||
417| this.showEmpty();
|
||||
418| }
|
||||
419| },
|
||||
420|
|
||||
421| showLoading() {
|
||||
422| const grid = document.getElementById("dashboard-bookmarks-grid");
|
||||
423| const empty = document.getElementById("dashboard-bookmarks-empty");
|
||||
424| const section = document.getElementById("dashboard-bookmarks-section");
|
||||
425|
|
||||
426| if (grid) grid.innerHTML = "";
|
||||
427| if (empty) empty.classList.add("hidden");
|
||||
428| },
|
||||
429|
|
||||
430| render() {
|
||||
431| const grid = document.getElementById("dashboard-bookmarks-grid");
|
||||
432| const empty = document.getElementById("dashboard-bookmarks-empty");
|
||||
433| const section = document.getElementById("dashboard-bookmarks-section");
|
||||
434|
|
||||
435| if (!this._cache || this._cache.length === 0) {
|
||||
436| if (grid) grid.innerHTML = "";
|
||||
437| if (empty) empty.classList.remove("hidden");
|
||||
438| return;
|
||||
439| }
|
||||
440|
|
||||
441| if (empty) empty.classList.add("hidden");
|
||||
442| if (!grid) return;
|
||||
443| grid.innerHTML = "";
|
||||
444|
|
||||
445| this._cache.forEach((f, idx) => {
|
||||
446| const card = DashboardRecentWidget._createCard(f, idx);
|
||||
447| grid.appendChild(card);
|
||||
448| });
|
||||
449|
|
||||
450| safeCreateIcons();
|
||||
451| },
|
||||
452|
|
||||
453| showEmpty() {
|
||||
454| const grid = document.getElementById("dashboard-bookmarks-grid");
|
||||
455| const empty = document.getElementById("dashboard-bookmarks-empty");
|
||||
456| if (grid) grid.innerHTML = "";
|
||||
457| if (empty) empty.classList.remove("hidden");
|
||||
458| }
|
||||
459|};
|
||||
460|
|
||||
461|export { DashboardRecentWidget, DashboardStatsWidget, DashboardBookmarkWidget, DashboardSharedWidget };
|
||||
462|
|
||||
Reference in New Issue
Block a user