diff --git a/backend/main.py b/backend/main.py index fffb41d..07f2fb9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2364,6 +2364,37 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative logger.error(f"PDF read error for {path}: {e}") raise HTTPException(status_code=500, detail=f"Error reading PDF: {e!s}") + # === Images: return as viewable image === + IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp", ".ico"} + if ext in IMAGE_EXTENSIONS: + size = file_path.stat().st_size + mime_map = { + ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", + ".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp", + ".bmp": "image/bmp", ".ico": "image/x-icon", + } + mime = mime_map.get(ext, "application/octet-stream") + html = ( + f'
' + f'' + f'
' + ) + return { + "vault": vault_name, + "path": path, + "title": file_path.name, + "tags": [], + "frontmatter": {}, + "html": html, + "raw_length": size, + "extension": ext, + "is_markdown": False, + "is_image": True, + "image_mime": mime, + "size_bytes": size, + } + try: raw = file_path.read_text(encoding="utf-8", errors="replace") except PermissionError as e: @@ -2388,7 +2419,57 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative except Exception as e: logger.error(f"Unexpected error reading file {path}: {e}") raise HTTPException(status_code=500, detail=f"Error reading file: {e!s}") - + + # === CSV: render as HTML table === + if ext == ".csv": + import csv, io as csv_io + reader = csv.reader(csv_io.StringIO(raw)) + rows = list(reader) + if not rows: + html = "

Fichier CSV vide

" + else: + headers = rows[0] + data_rows = rows[1:] + html = '
' + for h in headers: + html += f"" + html += "" + for row in data_rows: + html += "" + for cell in row: + html += f"" + html += "" + html += "
{h}
{cell}
" + return { + "vault": vault_name, "path": path, + "title": file_path.name, "tags": [], "frontmatter": {}, + "html": html, "raw_length": len(raw), "extension": ext, + "is_markdown": False, "is_csv": True, + } + + # === JSON: syntax-highlighted display === + if ext == ".json": + import json as json_mod + try: + parsed = json_mod.loads(raw) + formatted = json_mod.dumps(parsed, indent=2, ensure_ascii=False) + except json_mod.JSONDecodeError: + formatted = raw + html = f"
{html_mod.escape(formatted)}
" + return { + "vault": vault_name, "path": path, + "title": file_path.name, "tags": [], "frontmatter": {}, + "html": html, "raw_length": len(raw), "extension": ext, + "is_markdown": False, "is_json": True, + } + + # === Plain text / other readable files === + TEXT_EXTENSIONS = {".txt", ".log", ".yml", ".yaml", ".toml", ".ini", ".cfg", + ".sh", ".bash", ".py", ".js", ".ts", ".html", ".css", + ".xml", ".rst", ".tex", ".sql", ".conf", ".env"} + if ext in TEXT_EXTENSIONS or ext == ".md": + pass # handled below or by markdown section + if ext == ".md": post = parse_markdown_file(raw) diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/data/users.json b/data/users.json deleted file mode 100644 index cccdd1f..0000000 --- a/data/users.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 1, - "users": { - "admin": { - "id": "f7b85dfa-bcf6-4e42-b861-7563673620a6", - "username": "admin", - "display_name": "admin", - "password_hash": "$argon2id$v=19$m=65536,t=2,p=2$H4Pa7H4HOWeQoEbj4Ndkbw$c61e9zRHuXXdrLB6uTrjIBo/FFQaaZMaNHkB7CZaHhM", - "role": "admin", - "vaults": [ - "*" - ], - "active": true, - "created_at": "2026-03-23T19:38:00.742597+00:00", - "last_login": null, - "failed_attempts": 5, - "locked_until": "2026-06-02T14:01:43.472937+00:00" - } - } -} \ No newline at end of file diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js index cf8eaf8..3cd58ed 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -431,13 +431,78 @@ export function renderFile(data) {
${tocHtml} - +
`; lucide.createIcons(); return; } + // Handle images + if (data.is_image) { + const imgUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`; + area.innerHTML = ` +
+
+ ${escapeHtml(data.title)} + + +
+
+ ${data.html} +
+
`; + lucide.createIcons(); + return; + } + + // Handle CSV + if (data.is_csv) { + area.innerHTML = ` +
+
+ CSV — ${escapeHtml(data.title)} + +
+
${data.html}
+
`; + lucide.createIcons(); + return; + } + + // Handle JSON and plain text + if (data.is_json || data.extension === '.txt' || data.extension === '.log' || data.extension === '.yml' || data.extension === '.yaml' || data.extension === '.sh' || data.extension === '.py' || data.extension === '.js' || data.extension === '.toml' || data.extension === '.ini' || data.extension === '.cfg' || data.extension === '.conf' || data.extension === '.env') { + // Read raw content for plain text + fetch(`/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`) + .then(r => r.text()) + .then(text => { + const extClass = `lang-${data.extension.replace('.', '')}`; + area.innerHTML = ` +
+
+ ${escapeHtml(data.title)} + +
+
+
${escapeHtml(text)}
+
+
`; + lucide.createIcons(); + }) + .catch(() => { + area.innerHTML = `

Erreur lors du chargement du fichier.

`; + }); + return; + } + // Handle unsupported (binary) files if (data.unsupported) { const sizeStr = data.size_bytes diff --git a/frontend/style.css b/frontend/style.css index ae2d9f5..fcf8694 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -8539,6 +8539,97 @@ body.popup-mode .content-area { margin-left: 4px; } +/* ── Image Viewer ── */ +.image-viewer-body { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; + background: var(--surface1); + padding: 20px; +} +.image-viewer-body img { + border-radius: 4px; + box-shadow: 0 2px 12px rgba(0,0,0,0.15); +} + +/* ── Text / CSV Viewer ── */ +.text-viewer-container { + display: flex; + flex-direction: column; + height: 100%; +} +.file-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--surface1); + border-bottom: 1px solid var(--border); + font-size: 0.85rem; + flex-shrink: 0; +} +.file-info { + flex: 1; + color: var(--text-dim); +} +.text-viewer-body { + flex: 1; + overflow: auto; + padding: 16px; + background: var(--surface2); +} +.text-viewer-body pre { + margin: 0; + font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + font-size: 0.85rem; + line-height: 1.5; + white-space: pre-wrap; +} +.text-viewer-body code { + color: var(--text); +} + +/* ── CSV Table ── */ +.csv-table-wrapper { + overflow: auto; + max-height: 70vh; +} +.csv-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} +.csv-table th { + background: var(--surface1); + color: var(--text-dim); + font-weight: 600; + padding: 8px 12px; + text-align: left; + border-bottom: 2px solid var(--border); + position: sticky; + top: 0; +} +.csv-table td { + padding: 6px 12px; + border-bottom: 1px solid var(--border-light, var(--border)); +} +.csv-table tr:hover td { + background: var(--surface1); +} + +/* ── JSON Viewer ── */ +.json-viewer { + font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + font-size: 0.85rem; + line-height: 1.5; + padding: 16px; + background: var(--surface2); + border-radius: 6px; + overflow: auto; + max-height: 70vh; +} + /* ── PaneManager — Split View ── */ .pane-grid { display: grid;