diff --git a/backend/main.py b/backend/main.py index 6fa200b..6710eb5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,6 @@ from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.responses import JSONResponse, HTMLResponse +import json as json_lib from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import Optional, List @@ -216,3 +218,70 @@ async def upload_favicon(file: UploadFile = File(...)): with open(dest, "wb") as f: shutil.copyfileobj(file.file, f) return {"favicon": filename} + + +@app.get("/export/json") +async def export_json(): + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute("SELECT * FROM categories ORDER BY position") as cur: + categories = [dict(r) for r in await cur.fetchall()] + for cat in categories: + async with db.execute( + "SELECT * FROM subcategories WHERE category_id=? ORDER BY position", + (cat["id"],) + ) as cur: + subs = [dict(r) for r in await cur.fetchall()] + for sub in subs: + async with db.execute( + "SELECT * FROM links WHERE subcategory_id=? ORDER BY position", + (sub["id"],) + ) as cur: + sub["links"] = [dict(r) for r in await cur.fetchall()] + cat["subcategories"] = subs + headers = {"Content-Disposition": "attachment; filename=startpage-bookmarks.json"} + return JSONResponse(content=categories, headers=headers) + +@app.get("/export/html") +async def export_html(): + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute("SELECT * FROM categories ORDER BY position") as cur: + categories = [dict(r) for r in await cur.fetchall()] + for cat in categories: + async with db.execute( + "SELECT * FROM subcategories WHERE category_id=? ORDER BY position", + (cat["id"],) + ) as cur: + subs = [dict(r) for r in await cur.fetchall()] + for sub in subs: + async with db.execute( + "SELECT * FROM links WHERE subcategory_id=? ORDER BY position", + (sub["id"],) + ) as cur: + sub["links"] = [dict(r) for r in await cur.fetchall()] + cat["subcategories"] = subs + + lines = [ + "", + "", + "", + "Bookmarks", + "

Bookmarks

", + "

", + ] + for cat in categories: + lines.append("

" + cat["name"] + "

") + lines.append("

") + for sub in cat["subcategories"]: + lines.append("

" + sub["name"] + "

") + lines.append("

") + for link in sub["links"]: + lines.append("

" + link["label"] + "") + lines.append("

") + lines.append("

") + lines.append("

") + + body = "\n".join(lines) + headers = {"Content-Disposition": "attachment; filename=startpage-bookmarks.html"} + return HTMLResponse(content=body, headers=headers) diff --git a/frontend/app.js b/frontend/app.js index f692565..4527542 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -421,3 +421,24 @@ document.getElementById('modal-overlay').addEventListener('click', (e) => { }); loadCategories(); + +// Export +function exportBookmarks(format) { + document.getElementById('export-dropdown').classList.remove('open'); + const a = document.createElement('a'); + a.href = API + '/export/' + format; + a.download = format === 'json' ? 'startpage-bookmarks.json' : 'startpage-bookmarks.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + +// Export dropdown toggle +document.getElementById('export-btn').addEventListener('click', (e) => { + e.stopPropagation(); + document.getElementById('export-dropdown').classList.toggle('open'); +}); + +document.addEventListener('click', () => { + document.getElementById('export-dropdown').classList.remove('open'); +}); diff --git a/frontend/index.html b/frontend/index.html index a76d65d..6922708 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,6 +18,19 @@ +
+ +
+ + +
+
diff --git a/frontend/style.css b/frontend/style.css index ef0b1bc..148a74d 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -420,3 +420,40 @@ body { .link-card.sortable-chosen { cursor: grabbing; } + +/* Export dropdown */ +.export-wrapper { + position: relative; +} +.export-dropdown { + display: none; + position: absolute; + top: calc(100% + 6px); + right: 0; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + min-width: 200px; + z-index: 50; + overflow: hidden; +} +.export-dropdown.open { + display: block; +} +.export-option { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 14px; + font-size: 13px; + color: var(--text-muted); + cursor: pointer; + background: none; + border: none; + width: 100%; + text-align: left; +} +.export-option:hover { + background: var(--surface2); + color: var(--accent); +}