diff --git a/backend/main.py b/backend/main.py
index 9823b90..19206cf 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -195,6 +195,39 @@ async def api_file_download(vault_name: str, path: str = Query(..., description=
)
+@app.put("/api/file/{vault_name}/save")
+async def api_file_save(vault_name: str, path: str = Query(..., description="Relative path to file"), body: dict = {}):
+ """Save file content."""
+ vault_data = get_vault_data(vault_name)
+ if not vault_data:
+ raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
+
+ vault_root = Path(vault_data["path"])
+ file_path = vault_root / path
+
+ # Security: ensure path is within vault
+ try:
+ file_path.resolve().relative_to(vault_root.resolve())
+ except ValueError:
+ raise HTTPException(status_code=403, detail="Access denied: path outside vault")
+
+ if not file_path.exists():
+ raise HTTPException(status_code=404, detail=f"File not found: {path}")
+
+ # Get content from body
+ content = body.get('content', '')
+
+ try:
+ file_path.write_text(content, encoding="utf-8")
+ logger.info(f"File saved: {vault_name}/{path}")
+ return {"status": "ok", "vault": vault_name, "path": path, "size": len(content)}
+ except PermissionError:
+ raise HTTPException(status_code=403, detail="Permission denied: vault may be read-only")
+ except Exception as e:
+ logger.error(f"Error saving file {vault_name}/{path}: {e}")
+ raise HTTPException(status_code=500, detail=f"Error saving file: {str(e)}")
+
+
@app.get("/api/file/{vault_name}")
async def api_file(vault_name: str, path: str = Query(..., description="Relative path to file")):
"""Return rendered HTML + metadata for a file."""
diff --git a/frontend/app.js b/frontend/app.js
index 14d3c69..89ec9ae 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -13,6 +13,9 @@
let cachedRawSource = null;
let allVaults = [];
let selectedContextVault = "all";
+ let editorView = null;
+ let editorVault = null;
+ let editorPath = null;
// ---------------------------------------------------------------------------
// File extension → Lucide icon mapping
@@ -476,6 +479,14 @@
document.body.removeChild(a);
});
+ const editBtn = el("button", { class: "btn-action", title: "Éditer" }, [
+ icon("edit", 14),
+ document.createTextNode("Éditer"),
+ ]);
+ editBtn.addEventListener("click", () => {
+ openEditor(data.vault, data.path);
+ });
+
// Frontmatter
let fmSection = null;
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
@@ -529,7 +540,7 @@
area.appendChild(el("div", { class: "file-header" }, [
el("div", { class: "file-title" }, [document.createTextNode(data.title)]),
tagsDiv,
- el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn]),
+ el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn, editBtn]),
]));
if (fmSection) area.appendChild(fmSection);
area.appendChild(mdDiv);
@@ -574,6 +585,8 @@
}
async function performSearch(query, vaultFilter, tagFilter) {
+ showLoading();
+
let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
@@ -754,18 +767,196 @@
safeCreateIcons();
}
+ function showLoading() {
+ const area = document.getElementById("content-area");
+ area.innerHTML = `
+
+
+
Recherche en cours...
+
`;
+ }
+
+ function goHome() {
+ const searchInput = document.getElementById("search-input");
+ if (searchInput) searchInput.value = "";
+
+ document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
+
+ currentVault = null;
+ currentPath = null;
+ showingSource = false;
+ cachedRawSource = null;
+
+ closeMobileSidebar();
+ showWelcome();
+ }
+
+ // ---------------------------------------------------------------------------
+ // 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;
+ }
+
+ // Wait for CodeMirror to be available
+ await waitForCodeMirror();
+
+ const { EditorView, EditorState, basicSetup, markdown, oneDark, keymap } = window.CodeMirror;
+
+ // Determine theme
+ const currentTheme = document.documentElement.getAttribute("data-theme");
+ const extensions = [
+ basicSetup,
+ markdown(),
+ keymap.of([{
+ key: "Mod-s",
+ run: () => {
+ saveFile();
+ return true;
+ }
+ }]),
+ EditorView.lineWrapping,
+ ];
+
+ if (currentTheme === "dark") {
+ extensions.push(oneDark);
+ }
+
+ const state = EditorState.create({
+ doc: rawData.raw,
+ extensions: extensions,
+ });
+
+ editorView = new EditorView({
+ state: state,
+ parent: bodyEl,
+ });
+
+ 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;
+ }
+ editorVault = null;
+ editorPath = null;
+ }
+
+ async function saveFile() {
+ if (!editorView || !editorVault || !editorPath) return;
+
+ const content = editorView.state.doc.toString();
+ const saveBtn = document.getElementById("editor-save");
+ const originalText = saveBtn.textContent;
+
+ try {
+ saveBtn.disabled = true;
+ saveBtn.innerHTML = ' Sauvegarde...';
+ safeCreateIcons();
+
+ const response = await fetch(
+ `/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content: content }),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || "Erreur de sauvegarde");
+ }
+
+ saveBtn.innerHTML = ' Sauvegardé !';
+ safeCreateIcons();
+
+ setTimeout(() => {
+ closeEditor();
+ // Reload the file if it's currently open
+ if (currentVault === editorVault && currentPath === editorPath) {
+ openFile(currentVault, currentPath);
+ }
+ }, 800);
+ } catch (err) {
+ console.error("Save error:", err);
+ alert(`Erreur: ${err.message}`);
+ saveBtn.innerHTML = originalText;
+ saveBtn.disabled = false;
+ safeCreateIcons();
+ }
+ }
+
+ function initEditor() {
+ const cancelBtn = document.getElementById("editor-cancel");
+ const saveBtn = document.getElementById("editor-save");
+ const modal = document.getElementById("editor-modal");
+
+ cancelBtn.addEventListener("click", closeEditor);
+ 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();
+ }
+ });
+ }
+
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
async function init() {
initTheme();
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
+ document.getElementById("header-logo").addEventListener("click", goHome);
initSearch();
initMobile();
initVaultContext();
initSidebarFilter();
initSidebarResize();
initTagResize();
+ initEditor();
try {
await Promise.all([loadVaults(), loadTags()]);
diff --git a/frontend/index.html b/frontend/index.html
index 9bc0249..208e8bb 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -9,6 +9,16 @@
+
+
+
@@ -19,7 +29,7 @@
-
+
+
+