466 lines
18 KiB
JavaScript
466 lines
18 KiB
JavaScript
// 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 } 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 = '<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 || 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 = `<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 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 };
|