Add export function — JSON and Netscape HTML bookmark format

This commit is contained in:
Einar 2026-05-11 14:41:13 +02:00
parent 2f0aca09e2
commit c64b64c571
4 changed files with 140 additions and 0 deletions

View file

@ -1,4 +1,6 @@
from fastapi import FastAPI, UploadFile, File, HTTPException 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 fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List from typing import Optional, List
@ -216,3 +218,70 @@ async def upload_favicon(file: UploadFile = File(...)):
with open(dest, "wb") as f: with open(dest, "wb") as f:
shutil.copyfileobj(file.file, f) shutil.copyfileobj(file.file, f)
return {"favicon": filename} 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 = [
"<!DOCTYPE NETSCAPE-Bookmark-file-1>",
"<!-- This is an automatically generated file. -->",
"<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">",
"<TITLE>Bookmarks</TITLE>",
"<H1>Bookmarks</H1>",
"<DL><p>",
]
for cat in categories:
lines.append(" <DT><H3>" + cat["name"] + "</H3>")
lines.append(" <DL><p>")
for sub in cat["subcategories"]:
lines.append(" <DT><H3>" + sub["name"] + "</H3>")
lines.append(" <DL><p>")
for link in sub["links"]:
lines.append(" <DT><A HREF=\"" + link["url"] + "\">" + link["label"] + "</A>")
lines.append(" </DL><p>")
lines.append(" </DL><p>")
lines.append("</DL>")
body = "\n".join(lines)
headers = {"Content-Disposition": "attachment; filename=startpage-bookmarks.html"}
return HTMLResponse(content=body, headers=headers)

View file

@ -421,3 +421,24 @@ document.getElementById('modal-overlay').addEventListener('click', (e) => {
}); });
loadCategories(); 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');
});

View file

@ -18,6 +18,19 @@
<button id="add-category-btn" class="btn-ghost"> <button id="add-category-btn" class="btn-ghost">
<i class="ti ti-plus"></i> Add category <i class="ti ti-plus"></i> Add category
</button> </button>
<div class="export-wrapper">
<button id="export-btn" class="btn-ghost">
<i class="ti ti-download"></i> Export
</button>
<div class="export-dropdown" id="export-dropdown">
<button class="export-option" onclick="exportBookmarks('json')">
<i class="ti ti-braces"></i> JSON
</button>
<button class="export-option" onclick="exportBookmarks('html')">
<i class="ti ti-bookmark"></i> Browser bookmarks (HTML)
</button>
</div>
</div>
</div> </div>
<nav id="category-nav"></nav> <nav id="category-nav"></nav>
<div id="subcategory-bar"></div> <div id="subcategory-bar"></div>

View file

@ -420,3 +420,40 @@ body {
.link-card.sortable-chosen { .link-card.sortable-chosen {
cursor: grabbing; 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);
}