ajout de fonctionnalités

This commit is contained in:
2026-05-25 20:21:42 -04:00
parent e1fcbe9ce7
commit 370420aa00
7 changed files with 1412 additions and 6 deletions
+900 -1
View File
@@ -769,7 +769,7 @@
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^\w\s-]/g, "")
.replace(/[^\p{L}\p{N}\s-]/gu, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim() || "heading"
@@ -3723,6 +3723,7 @@
renderConfigFilters();
loadConfigFields();
loadDiagnostics();
loadAbout();
await loadHiddenFilesSettings();
safeCreateIcons();
});
@@ -4017,6 +4018,70 @@
});
}
// --- About Section ---
function loadAbout() {
const container = document.getElementById("config-about");
if (!container) return;
// Fetch health info for version
api("/api/health").then((health) => {
container.innerHTML = "";
const sections = [
{
title: "Application",
rows: [
["Nom", "ObsiGate"],
["Version", APP_VERSION],
["Version API", health.version || "—"],
["Statut", health.status || "—"],
],
},
{
title: "Environnement",
rows: [
["Vaults configurés", health.vaults || "—"],
["Fichiers indexés", health.total_files || "—"],
["Navigateur", navigator.userAgent.split(" ").pop()],
["Plateforme", navigator.platform || "—"],
["Langue", navigator.language || "—"],
],
},
{
title: "Composants",
rows: [
["Backend", "FastAPI (Python)"],
["Rendu Markdown", "mistune"],
["Surveillance fichiers", "watchdog"],
["Frontend", "Vanilla JavaScript"],
["Icônes", "Lucide Icons"],
["Coloration syntaxe", "highlight.js"],
["Éditeur", "CodeMirror 6"],
],
},
];
sections.forEach((section) => {
const div = document.createElement("div");
div.className = "config-diag-section";
const title = document.createElement("div");
title.className = "config-diag-section-title";
title.textContent = section.title;
div.appendChild(title);
section.rows.forEach(([label, value]) => {
const row = document.createElement("div");
row.className = "config-diag-row";
row.innerHTML = `<span class="diag-label">${label}</span><span class="diag-value">${value}</span>`;
div.appendChild(row);
});
container.appendChild(div);
});
}).catch(() => {
container.innerHTML = '<div class="config-diag-loading">Erreur de chargement</div>';
});
}
// --- Hidden Files Configuration ---
async function loadHiddenFilesSettings() {
@@ -5863,6 +5928,16 @@
this._menu.innerHTML = '';
// Copy path — available for all types
const pathToCopy = type === 'vault' ? vault : `${vault}/${path}`;
this._addItem('clipboard-copy', 'Copier le chemin', () => this._copyPath(pathToCopy), false);
// Graph view — available for all types
const graphPath = type === 'vault' ? '' : path;
this._addItem('git-graph', 'Vue Graphique', () => GraphViewManager.open(vault, graphPath, type), false);
this._addSeparator();
if (type === 'vault') {
this._addItem('folder-plus', 'Nouveau dossier', () => this._createDirectory(), isReadonly);
this._addItem('file-plus', 'Nouveau fichier', () => this._createFile(), isReadonly);
@@ -5951,6 +6026,14 @@
_deleteFile() {
FileOperations.confirmDeleteFile(this._targetVault, this._targetPath);
},
_copyPath(path) {
navigator.clipboard.writeText(path).then(() => {
showToast(`Chemin copié : ${path}`, 'success');
}).catch(() => {
showToast('Erreur lors de la copie', 'error');
});
}
};
@@ -6989,4 +7072,820 @@
init();
registerServiceWorker();
});
// ---------------------------------------------------------------------------
// Tab Manager — Multi-file tab support
// ---------------------------------------------------------------------------
const TabManager = {
_tabs: [],
_activeTabId: null,
_tabCache: {}, // { tabId: { vault, path, title, data, rawSource, sourceView, scrollTop, icon } }
_tabBar: null,
_tabList: null,
_dirtyTabs: new Set(),
init() {
this._tabBar = document.getElementById("tab-bar");
this._tabList = document.getElementById("tab-list");
},
/** Open a file in a tab (or focus existing) */
async open(vault, path, options = {}) {
const tabId = `${vault}::${path}`;
// If already open, just focus it
const existing = this._tabs.find(t => t.id === tabId);
if (existing) {
this.activate(tabId);
return;
}
// Create new tab
const name = path.split("/").pop().replace(/\.md$/i, "");
const icon = getFileIcon(name + ".md");
this._tabs.push({ id: tabId, vault, path, name, icon });
this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
this._renderTabs();
this.activate(tabId);
},
/** Activate a specific tab */
async activate(tabId) {
if (this._activeTabId === tabId && this._tabs.length > 0) return;
// Save current tab state
if (this._activeTabId && this._tabCache[this._activeTabId]) {
this._saveCurrentTabState();
}
this._activeTabId = tabId;
this._renderTabs();
// Load tab content
const cache = this._tabCache[tabId];
if (!cache) return;
// Update global state
currentVault = cache.vault;
currentPath = cache.path;
syncActiveFileTreeItem(cache.vault, cache.path);
const area = document.getElementById("content-area");
if (cache.data) {
// Use cached data
this._restoreTabContent(cache, area);
} else {
// Fetch file content
area.innerHTML = '<div style="padding:40px;text-align:center;color:var(--text-muted)">Chargement...</div>';
try {
const data = await api(`/api/file/${encodeURIComponent(cache.vault)}?path=${encodeURIComponent(cache.path)}`);
cache.data = data;
cache.title = data.title;
renderFile(cache.data);
// Restore source view if needed
if (cache.sourceView) {
await this._toggleSourceView(cache, area);
}
if (cache.scrollTop) {
area.scrollTop = cache.scrollTop;
}
} catch (err) {
area.innerHTML = `<div style="padding:40px;text-align:center;color:var(--text-error)">Erreur: ${escapeHtml(err.message)}</div>`;
}
}
// Update URL hash
if (history.pushState) {
history.pushState(null, "", `#/file/${encodeURIComponent(cache.vault)}/${encodeURIComponent(cache.path)}`);
}
// Hide dashboard
const dashboard = document.getElementById("dashboard-home");
if (dashboard) dashboard.style.display = "none";
},
/** Close a tab */
close(tabId) {
const idx = this._tabs.findIndex(t => t.id === tabId);
if (idx === -1) return;
this._tabs.splice(idx, 1);
delete this._tabCache[tabId];
this._dirtyTabs.delete(tabId);
if (this._tabs.length === 0) {
this._activeTabId = null;
this._showDashboard();
this._tabBar.hidden = true;
} else if (this._activeTabId === tabId) {
// Activate adjacent tab
const newIdx = Math.min(idx, this._tabs.length - 1);
this.activate(this._tabs[newIdx].id);
}
this._renderTabs();
},
/** Close all tabs */
closeAll() {
this._tabs = [];
this._tabCache = {};
this._dirtyTabs.clear();
this._activeTabId = null;
this._showDashboard();
this._tabBar.hidden = true;
},
/** Close tabs to the right */
closeRight(tabId) {
const idx = this._tabs.findIndex(t => t.id === tabId);
if (idx === -1) return;
const toClose = this._tabs.slice(idx + 1);
for (const tab of toClose) {
delete this._tabCache[tab.id];
this._dirtyTabs.delete(tab.id);
}
this._tabs = this._tabs.slice(0, idx + 1);
if (!this._tabs.find(t => t.id === this._activeTabId)) {
this.activate(tabId);
}
this._renderTabs();
},
/** Close other tabs */
closeOthers(tabId) {
const tab = this._tabs.find(t => t.id === tabId);
if (!tab) return;
for (const t of this._tabs) {
if (t.id !== tabId) {
delete this._tabCache[t.id];
this._dirtyTabs.delete(t.id);
}
}
this._tabs = [tab];
this.activate(tabId);
this._renderTabs();
},
/** Reorder tabs by drag and drop */
moveTab(fromIdx, toIdx) {
if (fromIdx === toIdx || fromIdx < 0 || toIdx < 0) return;
const tab = this._tabs.splice(fromIdx, 1)[0];
this._tabs.splice(toIdx, 0, tab);
this._renderTabs();
},
/** Save current tab state before switching */
_saveCurrentTabState() {
const cache = this._tabCache[this._activeTabId];
if (!cache) return;
const area = document.getElementById("content-area");
const rendered = document.getElementById("file-rendered-content");
cache.scrollTop = area.scrollTop;
cache.sourceView = rendered ? rendered.style.display === "none" : false;
},
/** Restore tab content from cache */
_restoreTabContent(cache, area) {
renderFile(cache.data);
if (cache.sourceView) {
this._restoreSourceView(cache, area);
}
if (cache.scrollTop) {
area.scrollTop = cache.scrollTop;
}
},
async _toggleSourceView(cache, area) {
const rendered = document.getElementById("file-rendered-content");
const raw = document.getElementById("file-raw-content");
if (!rendered || !raw) return;
if (!cache.rawSource) {
const rawData = await api(`/api/file/${encodeURIComponent(cache.vault)}/raw?path=${encodeURIComponent(cache.path)}`);
cache.rawSource = rawData.raw;
}
raw.textContent = cache.rawSource;
rendered.style.display = "none";
raw.style.display = "block";
},
_restoreSourceView(cache, area) {
requestAnimationFrame(() => {
const rendered = document.getElementById("file-rendered-content");
const raw = document.getElementById("file-raw-content");
if (rendered && raw && cache.rawSource) {
raw.textContent = cache.rawSource;
rendered.style.display = "none";
raw.style.display = "block";
}
});
},
_showDashboard() {
const area = document.getElementById("content-area");
area.innerHTML = "";
const dashboard = document.getElementById("dashboard-home");
if (dashboard) {
dashboard.style.display = "";
area.appendChild(dashboard);
}
if (history.pushState) {
history.pushState(null, "", "#");
}
},
/** Render the tab bar */
_renderTabs() {
if (!this._tabList) return;
this._tabList.innerHTML = "";
if (this._tabs.length === 0) {
this._tabBar.hidden = true;
return;
}
this._tabBar.hidden = false;
this._tabs.forEach((tab, idx) => {
const el = document.createElement("div");
el.className = "tab-item" + (tab.id === this._activeTabId ? " active" : "");
el.draggable = true;
el.dataset.tabId = tab.id;
el.dataset.index = idx;
// Icon
const iconEl = document.createElement("i");
iconEl.setAttribute("data-lucide", tab.icon);
iconEl.className = "tab-icon";
iconEl.style.width = "14px";
iconEl.style.height = "14px";
el.appendChild(iconEl);
// Name
const nameEl = document.createElement("span");
nameEl.className = "tab-name";
nameEl.textContent = tab.name;
nameEl.title = `${tab.vault}/${tab.path}`;
el.appendChild(nameEl);
// Close button
const closeEl = document.createElement("span");
closeEl.className = "tab-close";
closeEl.innerHTML = '<i data-lucide="x" style="width:12px;height:12px"></i>';
closeEl.addEventListener("click", (e) => {
e.stopPropagation();
this.close(tab.id);
});
el.appendChild(closeEl);
// Click to activate
el.addEventListener("click", () => this.activate(tab.id));
// Double-click to close
el.addEventListener("dblclick", (e) => {
e.preventDefault();
this.close(tab.id);
});
// Middle-click to close
el.addEventListener("mousedown", (e) => {
if (e.button === 1) {
e.preventDefault();
this.close(tab.id);
}
});
// Context menu on tab
el.addEventListener("contextmenu", (e) => {
e.preventDefault();
this._showTabContextMenu(e.clientX, e.clientY, tab.id);
});
// Drag and drop
el.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("text/plain", String(idx));
el.classList.add("dragging");
});
el.addEventListener("dragend", () => {
el.classList.remove("dragging");
document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
});
el.addEventListener("dragover", (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const rect = el.getBoundingClientRect();
const mid = rect.left + rect.width / 2;
document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
const indicator = document.createElement("div");
indicator.className = "tab-drop-indicator";
if (e.clientX < mid) {
el.before(indicator);
} else {
el.after(indicator);
}
});
el.addEventListener("drop", (e) => {
e.preventDefault();
document.querySelectorAll(".tab-drop-indicator").forEach(d => d.remove());
const fromIdx = parseInt(e.dataTransfer.getData("text/plain"));
const rect = el.getBoundingClientRect();
const mid = rect.left + rect.width / 2;
const toIdx = e.clientX < mid ? idx : idx + 1;
if (fromIdx !== toIdx && fromIdx !== toIdx - 1) {
this.moveTab(fromIdx, toIdx);
}
});
this._tabList.appendChild(el);
});
safeCreateIcons();
},
_showTabContextMenu(x, y, tabId) {
const existing = document.getElementById("tab-context-menu");
if (existing) existing.remove();
const menu = document.createElement("div");
menu.id = "tab-context-menu";
menu.className = "context-menu active";
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.innerHTML = `
<div class="context-menu-item" data-action="close"><i data-lucide="x" class="icon"></i><span>Fermer</span></div>
<div class="context-menu-item" data-action="closeOthers"><i data-lucide="x-circle" class="icon"></i><span>Fermer les autres</span></div>
<div class="context-menu-item" data-action="closeRight"><i data-lucide="arrow-right-circle" class="icon"></i><span>Fermer à droite</span></div>
<div class="context-menu-separator"></div>
<div class="context-menu-item" data-action="closeAll"><i data-lucide="trash-2" class="icon"></i><span>Fermer tout</span></div>
`;
document.body.appendChild(menu);
safeCreateIcons();
menu.addEventListener("click", (e) => {
const action = e.target.closest(".context-menu-item")?.dataset.action;
if (action === "close") this.close(tabId);
else if (action === "closeOthers") this.closeOthers(tabId);
else if (action === "closeRight") this.closeRight(tabId);
else if (action === "closeAll") this.closeAll();
menu.remove();
});
const closeMenu = () => menu.remove();
document.addEventListener("click", closeMenu, { once: true });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") { menu.remove(); } }, { once: true });
},
};
// ---- Modify openFile to use TabManager ----
const _originalOpenFile = openFile;
openFile = function(vault, path) {
TabManager.open(vault, path);
};
// ---- Keyboard shortcuts for tabs ----
document.addEventListener("keydown", (e) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === "w" || e.key === "W") {
e.preventDefault();
if (TabManager._activeTabId) {
TabManager.close(TabManager._activeTabId);
}
} else if (e.key === "Tab" && !e.shiftKey) {
e.preventDefault();
const tabs = TabManager._tabs;
const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
if (currentIdx >= 0 && tabs.length > 1) {
const nextIdx = (currentIdx + 1) % tabs.length;
TabManager.activate(tabs[nextIdx].id);
}
} else if (e.key === "Tab" && e.shiftKey) {
e.preventDefault();
const tabs = TabManager._tabs;
const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
if (currentIdx >= 0 && tabs.length > 1) {
const prevIdx = (currentIdx - 1 + tabs.length) % tabs.length;
TabManager.activate(tabs[prevIdx].id);
}
}
}
});
// ---- Modify init to include TabManager ----
const _origInit2 = init;
init = function() {
_origInit2();
TabManager.init();
};
// ---------------------------------------------------------------------------
// Graph View Manager — Interactive file/folder relationship visualization
// ---------------------------------------------------------------------------
const GraphViewManager = {
_canvas: null,
_ctx: null,
_nodes: [],
_edges: [],
_offsetX: 0,
_offsetY: 0,
_zoom: 1,
_dragging: false,
_dragNode: null,
_panning: false,
_panStartX: 0,
_panStartY: 0,
_animFrame: null,
_vault: null,
_path: null,
_nodePositions: {},
_width: 0,
_height: 0,
async open(vault, path, type) {
this._vault = vault;
this._path = path;
const modal = document.getElementById("graph-modal");
const title = document.getElementById("graph-title");
const info = document.getElementById("graph-info");
const canvas = document.getElementById("graph-canvas");
if (!modal || !canvas) return;
title.textContent = `Vue Graphique — ${vault}${path ? "/" + path : ""}`;
info.textContent = "Chargement...";
modal.classList.add("active");
this._canvas = canvas;
this._ctx = canvas.getContext("2d");
this._resetView();
// Fetch graph data
try {
const data = await api(`/api/graph/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}&depth=1`);
this._nodes = data.nodes || [];
this._edges = data.edges || [];
info.textContent = `${this._nodes.length} nœuds, ${this._edges.length} liens`;
this._initLayout();
this._startRender();
} catch (err) {
info.textContent = "Erreur de chargement";
console.error("Graph error:", err);
}
safeCreateIcons();
},
close() {
const modal = document.getElementById("graph-modal");
if (modal) modal.classList.remove("active");
if (this._animFrame) {
cancelAnimationFrame(this._animFrame);
this._animFrame = null;
}
},
_resetView() {
this._offsetX = 0;
this._offsetY = 0;
this._zoom = 1;
this._nodePositions = {};
},
_initLayout() {
const w = this._canvas.parentElement.clientWidth;
const h = this._canvas.parentElement.clientHeight;
this._canvas.width = w * devicePixelRatio;
this._canvas.height = h * devicePixelRatio;
this._canvas.style.width = w + "px";
this._canvas.style.height = h + "px";
this._width = w;
this._height = h;
this._ctx.scale(devicePixelRatio, devicePixelRatio);
// Position nodes in a circle initially
const cx = w / 2;
const cy = h / 2;
const radius = Math.min(w, h) * 0.35;
this._nodes.forEach((node, i) => {
const angle = (2 * Math.PI * i) / this._nodes.length;
this._nodePositions[node.id] = {
x: cx + radius * Math.cos(angle),
y: cy + radius * Math.sin(angle),
vx: 0,
vy: 0,
};
});
},
_startRender() {
const self = this;
let lastTime = 0;
const loop = (time) => {
const dt = Math.min((time - lastTime) / 1000, 0.1);
lastTime = time;
self._simulate(dt);
self._draw();
self._animFrame = requestAnimationFrame(loop);
};
this._animFrame = requestAnimationFrame(loop);
},
_simulate(dt) {
if (this._dragging || this._dragNode) return;
const positions = this._nodePositions;
const cx = this._width / 2;
const cy = this._height / 2;
// Spring forces (edges)
for (const edge of this._edges) {
const a = positions[edge.source];
const b = positions[edge.target];
if (!a || !b) continue;
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const targetLen = 80;
const force = (dist - targetLen) * 0.01;
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx += fx;
a.vy += fy;
b.vx -= fx;
b.vy -= fy;
}
// Repulsion between all nodes
for (const n1 of this._nodes) {
for (const n2 of this._nodes) {
if (n1.id === n2.id) continue;
const a = positions[n1.id];
const b = positions[n2.id];
if (!a || !b) continue;
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const force = 2000 / (dist * dist);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx -= fx;
a.vy -= fy;
}
}
// Center gravity
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
p.vx += (cx - p.x) * 0.001;
p.vy += (cy - p.y) * 0.001;
}
// Apply velocities with damping
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
p.vx *= 0.9;
p.vy *= 0.9;
p.x += p.vx * dt * 60;
p.y += p.vy * dt * 60;
}
},
_draw() {
const ctx = this._ctx;
const w = this._width;
const h = this._height;
ctx.save();
ctx.clearRect(0, 0, w, h);
// Apply transform (pan + zoom)
ctx.translate(this._offsetX, this._offsetY);
ctx.scale(this._zoom, this._zoom);
// Draw edges
for (const edge of this._edges) {
const a = this._nodePositions[edge.source];
const b = this._nodePositions[edge.target];
if (!a || !b) continue;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = edge.relation === "wikilink" ? "var(--accent-color, #2563eb)" : "var(--text-muted, #888)";
ctx.lineWidth = edge.relation === "wikilink" ? 2 : 1;
ctx.setLineDash(edge.relation === "wikilink" ? [4, 4] : []);
ctx.stroke();
}
ctx.setLineDash([]);
// Draw nodes
for (const node of this._nodes) {
const p = this._nodePositions[node.id];
if (!p) continue;
const r = Math.max(5, Math.min(20, 6 + Math.sqrt(node.size || 100) / 100));
// Node circle
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
switch (node.type) {
case "directory":
ctx.fillStyle = "#5b9bd5";
break;
case "file":
ctx.fillStyle = (node.path || "").endsWith(".md") ? "#70ad47" : "#c0c0c0";
break;
default:
ctx.fillStyle = "#ffc000";
break;
}
ctx.fill();
ctx.strokeStyle = "var(--bg-primary, #1e1e1e)";
ctx.lineWidth = 1.5;
ctx.stroke();
// Node label
const label = node.name.length > 20 ? node.name.slice(0, 18) + "..." : node.name;
ctx.font = `${11 / this._zoom}px -apple-system, sans-serif`;
ctx.fillStyle = "var(--text-primary, #ddd)";
ctx.textAlign = "center";
ctx.fillText(label, p.x, p.y + r + 12 / this._zoom);
}
ctx.restore();
},
_getNodeAt(screenX, screenY) {
const x = (screenX - this._offsetX) / this._zoom;
const y = (screenY - this._offsetY) / this._zoom;
for (const node of this._nodes) {
const p = this._nodePositions[node.id];
if (!p) continue;
const r = Math.max(5, Math.min(20, 6 + Math.sqrt(node.size || 100) / 100));
const dx = x - p.x;
const dy = y - p.y;
if (dx * dx + dy * dy <= r * r + 100) {
return { node, pos: p };
}
}
return null;
},
_onMouseDown(e) {
const rect = this._canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const hit = this._getNodeAt(mx, my);
if (hit) {
this._dragging = true;
this._dragNode = hit;
this._canvas.style.cursor = "grabbing";
} else {
this._panning = true;
this._panStartX = e.clientX - this._offsetX;
this._panStartY = e.clientY - this._offsetY;
this._canvas.style.cursor = "grabbing";
}
},
_onMouseMove(e) {
const rect = this._canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (this._dragging && this._dragNode) {
this._dragNode.pos.x = (mx - this._offsetX) / this._zoom;
this._dragNode.pos.y = (my - this._offsetY) / this._zoom;
this._dragNode.pos.vx = 0;
this._dragNode.pos.vy = 0;
} else if (this._panning) {
this._offsetX = e.clientX - this._panStartX;
this._offsetY = e.clientY - this._panStartY;
} else {
const hit = this._getNodeAt(mx, my);
this._canvas.style.cursor = hit ? "pointer" : "grab";
this._canvas.title = hit ? `Ouvrir: ${hit.node.path || hit.node.name}` : "";
}
},
_onMouseUp(e) {
if (this._dragging && this._dragNode) {
// Check if it was a click (not a drag)
const rect = this._canvas.getBoundingClientRect();
const node = this._dragNode.node;
this._dragging = false;
this._dragNode = null;
this._canvas.style.cursor = "grab";
// If it's a file, open it on click
if (node.type === "file") {
this.close();
openFile(this._vault, node.path);
} else if (node.type === "directory" || node.type === "vault") {
// Expand into this directory
this.close();
this.open(this._vault, node.path, node.type);
}
}
this._panning = false;
},
_onWheel(e) {
e.preventDefault();
const rect = this._canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
const newZoom = Math.max(0.2, Math.min(3, this._zoom * zoomFactor));
this._offsetX = mx - (mx - this._offsetX) * (newZoom / this._zoom);
this._offsetY = my - (my - this._offsetY) * (newZoom / this._zoom);
this._zoom = newZoom;
},
_onResize() {
if (!this._canvas || !this._nodes.length) return;
const w = this._canvas.parentElement.clientWidth;
const h = this._canvas.parentElement.clientHeight;
this._canvas.width = w * devicePixelRatio;
this._canvas.height = h * devicePixelRatio;
this._canvas.style.width = w + "px";
this._canvas.style.height = h + "px";
this._width = w;
this._height = h;
this._ctx.setTransform(1, 0, 0, 1, 0, 0);
this._ctx.scale(devicePixelRatio, devicePixelRatio);
},
};
// Init graph view event listeners after DOM ready
function initGraphView() {
const closeBtn = document.getElementById("graph-close");
const zoomIn = document.getElementById("graph-zoom-in");
const zoomOut = document.getElementById("graph-zoom-out");
const reset = document.getElementById("graph-reset");
const modal = document.getElementById("graph-modal");
const canvas = document.getElementById("graph-canvas");
if (closeBtn) closeBtn.addEventListener("click", () => GraphViewManager.close());
if (modal) modal.addEventListener("click", (e) => { if (e.target === modal) GraphViewManager.close(); });
if (zoomIn) zoomIn.addEventListener("click", () => { GraphViewManager._zoom = Math.min(3, GraphViewManager._zoom * 1.2); });
if (zoomOut) zoomOut.addEventListener("click", () => { GraphViewManager._zoom = Math.max(0.2, GraphViewManager._zoom * 0.8); });
if (reset) reset.addEventListener("click", () => {
GraphViewManager._offsetX = 0;
GraphViewManager._offsetY = 0;
GraphViewManager._zoom = 1;
});
if (canvas) {
canvas.addEventListener("mousedown", (e) => GraphViewManager._onMouseDown(e));
canvas.addEventListener("mousemove", (e) => GraphViewManager._onMouseMove(e));
canvas.addEventListener("mouseup", (e) => GraphViewManager._onMouseUp(e));
canvas.addEventListener("mouseleave", () => {
GraphViewManager._dragging = false;
GraphViewManager._dragNode = null;
GraphViewManager._panning = false;
canvas.style.cursor = "grab";
});
canvas.addEventListener("wheel", (e) => GraphViewManager._onWheel(e), { passive: false });
window.addEventListener("resize", () => GraphViewManager._onResize());
}
document.addEventListener("keydown", (e) => {
if (modal && modal.classList.contains("active") && e.key === "Escape") {
GraphViewManager.close();
}
});
}
// Add to init
const _origInit = init;
init = function() {
_origInit();
initGraphView();
};
})();
+47
View File
@@ -348,6 +348,11 @@
<!-- Sidebar resize handle -->
<div class="sidebar-resize-handle" id="sidebar-resize-handle"></div>
<!-- Tab bar -->
<div class="tab-bar" id="tab-bar" hidden>
<div class="tab-list" id="tab-list"></div>
</div>
<!-- Content -->
<main class="content-area" id="content-area" aria-label="Contenu principal">
<div id="dashboard-home" class="dashboard-home" role="region" aria-label="Tableau de bord">
@@ -620,6 +625,48 @@
<button class="config-btn-secondary" id="cfg-refresh-diag" style="margin-top:8px">Rafraîchir</button>
</section>
<!-- À propos -->
<section class="config-section">
<h2>📦 À propos</h2>
<div id="config-about" class="config-diagnostics">
<div class="config-diag-loading">Chargement...</div>
</div>
</section>
</div>
</div>
</div>
</div>
<!-- Graph View Modal -->
<div class="editor-modal" id="graph-modal">
<div class="editor-container" style="max-width:90vw;width:900px;height:80vh;display:flex;flex-direction:column;">
<div class="editor-header">
<div class="editor-title" id="graph-title">Vue Graphique</div>
<div class="editor-actions">
<span id="graph-info" style="font-size:0.75rem;color:var(--text-muted);margin-right:12px"></span>
<button class="editor-btn" id="graph-zoom-in" title="Zoom avant" aria-label="Zoom avant">
<i data-lucide="zoom-in" style="width:16px;height:16px"></i>
</button>
<button class="editor-btn" id="graph-zoom-out" title="Zoom arrière" aria-label="Zoom arrière">
<i data-lucide="zoom-out" style="width:16px;height:16px"></i>
</button>
<button class="editor-btn" id="graph-reset" title="Réinitialiser la vue" aria-label="Réinitialiser">
<i data-lucide="maximize" style="width:16px;height:16px"></i>
</button>
<button class="editor-btn" id="graph-close" title="Fermer" aria-label="Fermer">
<i data-lucide="x" style="width:16px;height:16px"></i>
</button>
</div>
</div>
<div class="editor-body" style="flex:1;overflow:hidden;position:relative;padding:0">
<canvas id="graph-canvas" style="width:100%;height:100%;cursor:grab"></canvas>
<div id="graph-legend" style="position:absolute;bottom:12px;left:12px;display:flex;gap:16px;font-size:0.7rem;color:var(--text-muted);background:var(--bg-primary);padding:6px 12px;border-radius:6px;border:1px solid var(--border-color)">
<span>🟦 Dossier</span>
<span>🟩 Fichier .md</span>
<span>⬜ Autre fichier</span>
<span style="color:var(--text-muted)">── Parent</span>
<span style="color:var(--accent-color,#2563eb)">┅┅ Wikilink</span>
</div>
</div>
</div>
+64 -2
View File
@@ -174,6 +174,57 @@
function safeCreateIcons() { if (window.lucide) window.lucide.createIcons(); }
// ---- Login form for popout auth redirect ----
function showLoginForm(vault, path) {
const loading = document.getElementById('loading');
loading.innerHTML = `
<div class="popout-login-card" style="text-align:center;max-width:360px;padding:24px;background:var(--bg-secondary);border-radius:8px;box-shadow:0 4px 12px var(--shadow-color)">
<div style="font-size:2rem;margin-bottom:8px">🔐</div>
<h3 style="margin:0 0 4px;color:var(--text-primary)">Authentification requise</h3>
<p style="margin:0 0 16px;color:var(--text-muted);font-size:0.85rem">Connectez-vous pour accéder à ce fichier</p>
<form id="popout-login-form" style="display:flex;flex-direction:column;gap:10px">
<input type="text" id="popout-username" placeholder="Nom d'utilisateur" required autocomplete="username"
style="padding:10px;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:0.9rem">
<input type="password" id="popout-password" placeholder="Mot de passe" required autocomplete="current-password"
style="padding:10px;border:1px solid var(--border-color);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:0.9rem">
<div id="popout-login-error" style="color:var(--text-error,#e74c3c);font-size:0.8rem;display:none"></div>
<button type="submit"
style="padding:10px;background:var(--accent-color,#2563eb);color:white;border:none;border-radius:6px;cursor:pointer;font-size:0.9rem">Se connecter</button>
</form>
</div>
`;
document.getElementById('popout-login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('popout-username').value;
const password = document.getElementById('popout-password').value;
const errorEl = document.getElementById('popout-login-error');
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
credentials: 'include',
});
if (!res.ok) {
const err = await res.json();
errorEl.textContent = err.detail || 'Identifiants invalides';
errorEl.style.display = 'block';
return;
}
// Login successful — reload the page to try loading the file again
window.location.reload();
} catch (err) {
errorEl.textContent = 'Erreur réseau';
errorEl.style.display = 'block';
}
});
}
// ---- Outline / TOC Manager ----
const OutlineManager = {
/**
* Slugify text to create valid IDs
@@ -183,7 +234,7 @@
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^\w\s-]/g, '')
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim() || 'heading';
@@ -598,6 +649,12 @@ const RightSidebarManager = {
const response = await fetch(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
credentials: 'include'
});
if (response.status === 401) {
showLoginForm(vault, path);
return;
}
if (!response.ok) throw new Error("Erreur lors du chargement (Essayez de vous reconnecter sur le site principal)");
const data = await response.json();
@@ -772,7 +829,12 @@ const RightSidebarManager = {
}, 300);
} catch (err) {
document.getElementById('loading').textContent = err.message;
// Check if this was an auth error from a later fetch
if (err.message && err.message.includes('401')) {
showLoginForm(vault, path);
} else {
document.getElementById('loading').textContent = err.message || 'Erreur de chargement';
}
}
}
+104
View File
@@ -5365,3 +5365,107 @@ body.popup-mode .content-area {
flex-shrink: 0;
margin-top: 2px;
}
/* ===== TAB BAR ===== */
.tab-bar {
display: flex;
align-items: stretch;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
overflow: hidden;
flex-shrink: 0;
min-height: 36px;
}
.tab-bar[hidden] {
display: none;
}
.tab-list {
display: flex;
align-items: stretch;
overflow-x: auto;
overflow-y: hidden;
flex: 1;
scrollbar-width: thin;
}
.tab-list::-webkit-scrollbar {
height: 3px;
}
.tab-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 0.8rem;
color: var(--text-muted);
cursor: pointer;
white-space: nowrap;
border-right: 1px solid var(--border);
background: transparent;
transition: background 0.15s, color 0.15s;
user-select: none;
position: relative;
min-width: 0;
flex-shrink: 0;
}
.tab-item:hover {
background: var(--bg-hover);
color: var(--text-secondary);
}
.tab-item.active {
background: var(--bg-primary);
color: var(--text-primary);
border-bottom: 2px solid var(--accent);
margin-bottom: -1px;
}
.tab-item .tab-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
opacity: 0.7;
}
.tab-item .tab-name {
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.tab-item .tab-close {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
}
.tab-item:hover .tab-close,
.tab-item.active .tab-close {
opacity: 0.6;
}
.tab-item .tab-close:hover {
opacity: 1;
background: var(--bg-hover);
}
.tab-item.dragging {
opacity: 0.5;
background: var(--bg-hover);
}
.tab-drop-indicator {
width: 2px;
background: var(--accent);
flex-shrink: 0;
}