Add export function — JSON and Netscape HTML bookmark format
This commit is contained in:
parent
2f0aca09e2
commit
c64b64c571
4 changed files with 140 additions and 0 deletions
|
|
@ -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 = [
|
||||
"<!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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,19 @@
|
|||
<button id="add-category-btn" class="btn-ghost">
|
||||
<i class="ti ti-plus"></i> Add category
|
||||
</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>
|
||||
<nav id="category-nav"></nav>
|
||||
<div id="subcategory-bar"></div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue