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 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)
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue