feat: PDF support + Split View (P1 roadmap items)

#74 — Support PDF complet:
- backend/pdf_reader.py — pymupdf/pypdf text extraction
- .pdf added to SUPPORTED_EXTENSIONS in indexer.py
- PDF-aware file view API endpoint (metadata, text preview)
- Streaming endpoint /api/file/{vault}/pdf/stream
- Frontend PDF viewer with iframe, toolbar, download
- CSS for PDF viewer container

#75 — Split View (PaneManager):
- New module frontend/js/pane-manager.js — ES module
- 1-4 pane CSS grid with resize handles
- Split right/down via keyboard (Ctrl+Alt+\) or PaneManager API
- Persistence in localStorage (obsigate-panes)
- Collapse to single pane, per-pane active state

285 tests passent
This commit is contained in:
2026-07-23 17:33:51 -04:00
parent 5b956f8d95
commit 93d0bda627
8 changed files with 506 additions and 19 deletions
+45
View File
@@ -2301,6 +2301,32 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
ext = file_path.suffix.lower()
# === PDF: special handling before read_text (binary file) ===
if ext == ".pdf":
try:
from backend.pdf_reader import extract_pdf_text, extract_pdf_metadata
pdf_text = extract_pdf_text(file_path, max_chars=100000)
pdf_meta = extract_pdf_metadata(file_path)
size = file_path.stat().st_size
return {
"vault": vault_name,
"path": path,
"title": pdf_meta.get("title") or file_path.name,
"tags": [],
"frontmatter": {},
"html": f"<div class='pdf-viewer'><p>PDF — {pdf_meta.get('pages', '?')} pages</p><pre>{pdf_text[:5000]}</pre></div>",
"raw_length": size,
"extension": ext,
"is_markdown": False,
"is_pdf": True,
"unsupported": False,
"pdf_metadata": pdf_meta,
"size_bytes": size,
}
except Exception as e:
logger.error(f"PDF read error for {path}: {e}")
raise HTTPException(status_code=500, detail=f"Error reading PDF: {str(e)}")
try:
raw = file_path.read_text(encoding="utf-8", errors="replace")
except PermissionError as e:
@@ -2365,6 +2391,25 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
}
@app.get("/api/file/{vault_name}/pdf/stream")
async def api_pdf_stream(vault_name: str, path: str = Query(...)):
"""Stream a PDF file with Content-Type: application/pdf for inline browser viewing."""
if not check_vault_access(vault_name, current_user):
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
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 = _resolve_safe_path(vault_root, path)
if not file_path.exists() or not file_path.is_file():
raise HTTPException(status_code=404, detail=f"File not found: {path}")
if file_path.suffix.lower() != ".pdf":
raise HTTPException(status_code=400, detail="Not a PDF file")
from fastapi.responses import FileResponse
return FileResponse(str(file_path), media_type="application/pdf", headers={
"Content-Disposition": f"inline; filename=\"{file_path.name}\""})
@app.get("/api/search", response_model=SearchResponse)
async def api_search(
q: str = Query("", description="Search query"),