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.
511 lines
13 KiB
JavaScript
511 lines
13 KiB
JavaScript
import * as State from "./state.js";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// File extension → Lucide icon mapping
|
|
// ---------------------------------------------------------------------------
|
|
const EXT_ICONS = {
|
|
// Text files
|
|
".md": "file-text",
|
|
".txt": "file-text",
|
|
".log": "file-text",
|
|
".readme": "file-text",
|
|
".rst": "file-text",
|
|
".adoc": "file-text",
|
|
|
|
// Web development
|
|
".html": "file-code",
|
|
".htm": "file-code",
|
|
".css": "file-code",
|
|
".scss": "file-code",
|
|
".sass": "file-code",
|
|
".less": "file-code",
|
|
".js": "file-code",
|
|
".jsx": "file-code",
|
|
".ts": "file-code",
|
|
".tsx": "file-code",
|
|
".vue": "file-code",
|
|
".svelte": "file-code",
|
|
|
|
// Programming languages
|
|
".py": "file-code",
|
|
".java": "file-code",
|
|
".c": "file-code",
|
|
".cpp": "file-code",
|
|
".cc": "file-code",
|
|
".cxx": "file-code",
|
|
".h": "file-code",
|
|
".hpp": "file-code",
|
|
".cs": "file-code",
|
|
".go": "file-code",
|
|
".rs": "file-code",
|
|
".rb": "file-code",
|
|
".php": "file-code",
|
|
".swift": "file-code",
|
|
".kt": "file-code",
|
|
".scala": "file-code",
|
|
".r": "file-code",
|
|
".m": "file-code",
|
|
".pl": "file-code",
|
|
".lua": "file-code",
|
|
".dart": "file-code",
|
|
".nim": "file-code",
|
|
".zig": "file-code",
|
|
".odin": "file-code",
|
|
".v": "file-code",
|
|
".cr": "file-code",
|
|
".ex": "file-code",
|
|
".exs": "file-code",
|
|
".elm": "file-code",
|
|
".purs": "file-code",
|
|
".hs": "file-code",
|
|
".ml": "file-code",
|
|
".ocaml": "file-code",
|
|
".fs": "file-code",
|
|
".fsx": "file-code",
|
|
".vb": "file-code",
|
|
".pas": "file-code",
|
|
".pp": "file-code",
|
|
".inc": "file-code",
|
|
|
|
// Data formats
|
|
".json": "file-json",
|
|
".yaml": "file-cog",
|
|
".yml": "file-cog",
|
|
".toml": "file-cog",
|
|
".xml": "file-code",
|
|
".csv": "table",
|
|
".tsv": "table",
|
|
".sql": "database",
|
|
".db": "database",
|
|
".sqlite": "database",
|
|
".sqlite3": "database",
|
|
".parquet": "database",
|
|
".avro": "database",
|
|
|
|
// Configuration files
|
|
".ini": "file-cog",
|
|
".cfg": "file-cog",
|
|
".conf": "file-cog",
|
|
".env": "file-cog",
|
|
".dockerfile": "file-cog",
|
|
".gitignore": "file-cog",
|
|
".gitattributes": "file-cog",
|
|
".editorconfig": "file-cog",
|
|
".eslintrc": "file-cog",
|
|
".prettierrc": "file-cog",
|
|
".babelrc": "file-cog",
|
|
".tsconfig": "file-cog",
|
|
"package.json": "file-cog",
|
|
"package-lock.json": "file-cog",
|
|
"yarn.lock": "file-cog",
|
|
"composer.json": "file-cog",
|
|
"requirements.txt": "file-cog",
|
|
"pipfile": "file-cog",
|
|
"gemfile": "file-cog",
|
|
"cargo.toml": "file-cog",
|
|
"go.mod": "file-cog",
|
|
"go.sum": "file-cog",
|
|
"pom.xml": "file-cog",
|
|
"build.gradle": "file-cog",
|
|
"cmakelists.txt": "file-cog",
|
|
"makefile": "file-cog",
|
|
|
|
// Shell scripts
|
|
".sh": "terminal",
|
|
".bash": "terminal",
|
|
".zsh": "terminal",
|
|
".fish": "terminal",
|
|
".bat": "terminal",
|
|
".cmd": "terminal",
|
|
".ps1": "terminal",
|
|
".psm1": "terminal",
|
|
".psd1": "terminal",
|
|
|
|
// Document formats
|
|
".pdf": "file-text",
|
|
".doc": "file-text",
|
|
".docx": "file-text",
|
|
".rtf": "file-text",
|
|
".odt": "file-text",
|
|
".tex": "file-text",
|
|
".latex": "file-text",
|
|
|
|
// Image files
|
|
".png": "file-image",
|
|
".jpg": "file-image",
|
|
".jpeg": "file-image",
|
|
".gif": "file-image",
|
|
".svg": "file-image",
|
|
".webp": "file-image",
|
|
".bmp": "file-image",
|
|
".ico": "file-image",
|
|
".tiff": "file-image",
|
|
".tif": "file-image",
|
|
|
|
// Audio files
|
|
".mp3": "file-music",
|
|
".wav": "file-music",
|
|
".flac": "file-music",
|
|
".aac": "file-music",
|
|
".ogg": "file-music",
|
|
".m4a": "file-music",
|
|
".wma": "file-music",
|
|
|
|
// Video files
|
|
".mp4": "play",
|
|
".avi": "play",
|
|
".mov": "play",
|
|
".wmv": "play",
|
|
".flv": "play",
|
|
".webm": "play",
|
|
".mkv": "play",
|
|
".m4v": "play",
|
|
".3gp": "play",
|
|
|
|
// Archive files
|
|
".zip": "file-archive",
|
|
".rar": "file-archive",
|
|
".7z": "file-archive",
|
|
".tar": "file-archive",
|
|
".gz": "file-archive",
|
|
".tgz": "file-archive",
|
|
".bz2": "file-archive",
|
|
".xz": "file-archive",
|
|
".deb": "file-archive",
|
|
".rpm": "file-archive",
|
|
".dmg": "file-archive",
|
|
".pkg": "file-archive",
|
|
".msi": "file-archive",
|
|
".exe": "file-archive",
|
|
|
|
// Font files
|
|
".ttf": "file-type",
|
|
".otf": "file-type",
|
|
".woff": "file-type",
|
|
".woff2": "file-type",
|
|
".eot": "file-type",
|
|
|
|
// Other common files
|
|
".key": "file-cog",
|
|
".pem": "file-cog",
|
|
".crt": "file-cog",
|
|
".cert": "file-cog",
|
|
".p12": "file-cog",
|
|
".pfx": "file-cog",
|
|
".lock": "file-cog",
|
|
".tmp": "file",
|
|
".bak": "file",
|
|
".old": "file",
|
|
".orig": "file",
|
|
".save": "file",
|
|
};
|
|
|
|
function getFileIcon(name) {
|
|
const ext = "." + name.split(".").pop().toLowerCase();
|
|
return EXT_ICONS[ext] || "file";
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Safe CDN helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let _iconDebounceTimer = null;
|
|
|
|
/**
|
|
* Debounced icon creation — batches multiple rapid calls into one
|
|
* DOM scan to avoid excessive reflows when building large trees.
|
|
*/
|
|
function safeCreateIcons() {
|
|
if (typeof lucide === "undefined" || !lucide.createIcons) return;
|
|
if (_iconDebounceTimer) return; // already scheduled
|
|
_iconDebounceTimer = requestAnimationFrame(() => {
|
|
_iconDebounceTimer = null;
|
|
try {
|
|
lucide.createIcons();
|
|
} catch (e) {
|
|
/* CDN not loaded */
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Force-flush icon creation immediately (use sparingly). */
|
|
function flushIcons() {
|
|
if (_iconDebounceTimer) {
|
|
cancelAnimationFrame(_iconDebounceTimer);
|
|
_iconDebounceTimer = null;
|
|
}
|
|
if (typeof lucide !== "undefined" && lucide.createIcons) {
|
|
try {
|
|
lucide.createIcons();
|
|
} catch (e) {
|
|
/* CDN not loaded */
|
|
}
|
|
}
|
|
}
|
|
|
|
function safeHighlight(block) {
|
|
if (typeof hljs !== "undefined" && hljs.highlightElement) {
|
|
try {
|
|
hljs.highlightElement(block);
|
|
} catch (e) {
|
|
/* CDN not loaded */
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
function escapeHtml(str) {
|
|
if (!str) return "";
|
|
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Editor (CodeMirror)
|
|
// ---------------------------------------------------------------------------
|
|
async function openEditor(vaultName, filePath) {
|
|
editorVault = vaultName;
|
|
editorPath = filePath;
|
|
|
|
const modal = document.getElementById("editor-modal");
|
|
const titleEl = document.getElementById("editor-title");
|
|
const bodyEl = document.getElementById("editor-body");
|
|
|
|
titleEl.textContent = `Édition: ${filePath.split("/").pop()}`;
|
|
|
|
// Fetch raw content
|
|
const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
|
|
const rawData = await api(rawUrl);
|
|
|
|
// Clear previous editor
|
|
bodyEl.innerHTML = "";
|
|
if (editorView) {
|
|
editorView.destroy();
|
|
editorView = null;
|
|
}
|
|
fallbackEditorEl = null;
|
|
|
|
try {
|
|
await waitForCodeMirror();
|
|
|
|
const { EditorView, EditorState, basicSetup, markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, oneDark, keymap } = window.CodeMirror;
|
|
|
|
const currentTheme = document.documentElement.getAttribute("data-theme");
|
|
const fileExt = filePath.split(".").pop().toLowerCase();
|
|
|
|
const extensions = [
|
|
basicSetup,
|
|
keymap.of([
|
|
{
|
|
key: "Mod-s",
|
|
run: () => {
|
|
saveFile();
|
|
return true;
|
|
},
|
|
},
|
|
]),
|
|
EditorView.lineWrapping,
|
|
];
|
|
|
|
// Add language support based on file extension
|
|
const langMap = {
|
|
md: markdown,
|
|
markdown: markdown,
|
|
py: python,
|
|
js: javascript,
|
|
jsx: javascript,
|
|
ts: javascript,
|
|
tsx: javascript,
|
|
mjs: javascript,
|
|
cjs: javascript,
|
|
html: html,
|
|
htm: html,
|
|
css: css,
|
|
scss: css,
|
|
less: css,
|
|
json: json,
|
|
xml: xml,
|
|
svg: xml,
|
|
sql: sql,
|
|
php: php,
|
|
cpp: cpp,
|
|
cc: cpp,
|
|
cxx: cpp,
|
|
c: cpp,
|
|
h: cpp,
|
|
hpp: cpp,
|
|
java: java,
|
|
rs: rust,
|
|
sh: javascript, // Using javascript for shell scripts as fallback
|
|
bash: javascript,
|
|
zsh: javascript,
|
|
};
|
|
|
|
const langMode = langMap[fileExt];
|
|
if (langMode) {
|
|
extensions.push(langMode());
|
|
}
|
|
|
|
if (currentTheme === "dark") {
|
|
extensions.push(oneDark);
|
|
}
|
|
|
|
const state = EditorState.create({
|
|
doc: rawData.raw,
|
|
extensions: extensions,
|
|
});
|
|
|
|
editorView = new EditorView({
|
|
state: state,
|
|
parent: bodyEl,
|
|
});
|
|
} catch (err) {
|
|
console.error("CodeMirror init failed, falling back to textarea:", err);
|
|
fallbackEditorEl = document.createElement("textarea");
|
|
fallbackEditorEl.className = "fallback-editor";
|
|
fallbackEditorEl.value = rawData.raw;
|
|
bodyEl.appendChild(fallbackEditorEl);
|
|
}
|
|
|
|
modal.classList.add("active");
|
|
safeCreateIcons();
|
|
}
|
|
|
|
async function waitForCodeMirror() {
|
|
let attempts = 0;
|
|
while (!window.CodeMirror && attempts < 50) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
attempts++;
|
|
}
|
|
if (!window.CodeMirror) {
|
|
throw new Error("CodeMirror failed to load");
|
|
}
|
|
}
|
|
|
|
function closeEditor() {
|
|
const modal = document.getElementById("editor-modal");
|
|
modal.classList.remove("active");
|
|
if (editorView) {
|
|
editorView.destroy();
|
|
editorView = null;
|
|
}
|
|
fallbackEditorEl = null;
|
|
editorVault = null;
|
|
editorPath = null;
|
|
}
|
|
|
|
async function saveFile() {
|
|
if ((!editorView && !fallbackEditorEl) || !editorVault || !editorPath) return;
|
|
|
|
const content = editorView ? editorView.state.doc.toString() : fallbackEditorEl.value;
|
|
const saveBtn = document.getElementById("editor-save");
|
|
const originalHTML = saveBtn.innerHTML;
|
|
|
|
try {
|
|
saveBtn.disabled = true;
|
|
saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
|
|
safeCreateIcons();
|
|
|
|
const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || "Erreur de sauvegarde");
|
|
}
|
|
|
|
saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>';
|
|
safeCreateIcons();
|
|
|
|
setTimeout(() => {
|
|
closeEditor();
|
|
if (currentVault === editorVault && currentPath === editorPath) {
|
|
openFile(currentVault, currentPath);
|
|
}
|
|
}, 800);
|
|
} catch (err) {
|
|
console.error("Save error:", err);
|
|
alert(`Erreur: ${err.message}`);
|
|
saveBtn.innerHTML = originalHTML;
|
|
saveBtn.disabled = false;
|
|
safeCreateIcons();
|
|
}
|
|
}
|
|
|
|
async function deleteFile() {
|
|
if (!editorVault || !editorPath) return;
|
|
|
|
const deleteBtn = document.getElementById("editor-delete");
|
|
const originalHTML = deleteBtn.innerHTML;
|
|
|
|
try {
|
|
deleteBtn.disabled = true;
|
|
deleteBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
|
|
safeCreateIcons();
|
|
|
|
const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}?path=${encodeURIComponent(editorPath)}`, { method: "DELETE" });
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || "Erreur de suppression");
|
|
}
|
|
|
|
closeEditor();
|
|
showWelcome();
|
|
await refreshSidebarForContext();
|
|
await refreshTagsForContext();
|
|
} catch (err) {
|
|
console.error("Delete error:", err);
|
|
alert(`Erreur: ${err.message}`);
|
|
deleteBtn.innerHTML = originalHTML;
|
|
deleteBtn.disabled = false;
|
|
safeCreateIcons();
|
|
}
|
|
}
|
|
|
|
function initEditor() {
|
|
const cancelBtn = document.getElementById("editor-cancel");
|
|
const deleteBtn = document.getElementById("editor-delete");
|
|
const saveBtn = document.getElementById("editor-save");
|
|
const modal = document.getElementById("editor-modal");
|
|
|
|
cancelBtn.addEventListener("click", closeEditor);
|
|
deleteBtn.addEventListener("click", deleteFile);
|
|
saveBtn.addEventListener("click", saveFile);
|
|
|
|
// Close on overlay click
|
|
modal.addEventListener("click", (e) => {
|
|
if (e.target === modal) {
|
|
closeEditor();
|
|
}
|
|
});
|
|
|
|
// ESC to close
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && modal.classList.contains("active")) {
|
|
closeEditor();
|
|
}
|
|
});
|
|
|
|
// Fix mouse wheel scrolling in editor
|
|
modal.addEventListener(
|
|
"wheel",
|
|
(e) => {
|
|
const editorBody = document.getElementById("editor-body");
|
|
if (editorBody && editorBody.contains(e.target)) {
|
|
// Let the editor handle the scroll
|
|
return;
|
|
}
|
|
// Prevent modal from scrolling if not in editor area
|
|
e.preventDefault();
|
|
},
|
|
{ passive: false },
|
|
);
|
|
}
|
|
|
|
export { getFileIcon, safeCreateIcons, flushIcons, safeHighlight, escapeHtml, EXT_ICONS, openEditor, closeEditor, saveFile, deleteFile, waitForCodeMirror, initEditor };
|