added custom template editor, cleaned up UI

- Replace CSS-only customization with full HTML template editing
- Users edit the entire page wrapper with {{content}} placeholder
- Add /style?reset escape hatch to recover from broken templates
- Move nav links to template, remove redundant nav from search page
- Delete remote pages when unsubscribing from an instance
This commit is contained in:
lichenblankie 2026-03-26 09:04:23 -07:00
parent 6f88b7cf57
commit 02450b0865
3 changed files with 44 additions and 36 deletions

View file

@ -2,15 +2,15 @@ import json
from datetime import datetime from datetime import datetime
from db import get_db, get_setting, set_setting, get_site_name, index_url, clean_url from db import get_db, get_setting, set_setting, get_site_name, index_url, clean_url
from templates import esc, snippet, wrap_page from templates import esc, snippet, wrap_page, DEFAULT_TEMPLATE
from rns_client import fetch_remote_sites from rns_client import fetch_remote_sites
def _respond(body_html, status=200): def _respond(body_html, status=200, use_default=False):
return { return {
"status": status, "status": status,
"content_type": "text/html; charset=utf-8", "content_type": "text/html; charset=utf-8",
"body": wrap_page(body_html), "body": wrap_page(body_html, use_default=use_default),
"headers": {}, "headers": {},
} }
@ -186,19 +186,13 @@ def handle_search(query):
if q and remote_rows: if q and remote_rows:
sub_count = f" + {len(remote_rows)} from subscriptions" sub_count = f" + {len(remote_rows)} from subscriptions"
return _respond( return _respond(
f'<h1><a href="/">{esc(name)}</a></h1>'
f'<form method="get" action="/">' f'<form method="get" action="/">'
f'<input name="q" value="{esc(q)}" placeholder="search your index" size="40">' f'<input name="q" value="{esc(q)}" placeholder="search your index" size="40">'
f' <button type="submit">search</button>' f' <button type="submit">search</button>'
f'</form>' f'</form>'
f'<p>{count} page(s) indexed.' f'<p class="meta">{count} pages indexed'
f' <a href="/add">+ add url</a>' f' · <a href="/add">+ add url</a></p>'
f' | <a href="/pages">browse</a>' f'{result_html}{trusted_html}{remote_html}'
f' | <a href="/tags">tags</a>'
f' | <a href="/subscriptions">subscriptions</a>'
f' | <a href="/style">customize</a>'
f' | <a href="/about">about</a></p>'
f'<hr>{result_html}{trusted_html}{remote_html}'
) )
@ -366,8 +360,11 @@ def handle_import_submit(body):
return handle_import_form(f"Imported {imported} page(s). {errors} error(s).") return handle_import_form(f"Imported {imported} page(s). {errors} error(s).")
def handle_style_form(msg=""): def handle_style_form(msg="", query=None):
css = get_setting("custom_css") if query and "reset" in query:
set_setting("custom_template", "")
msg = "Template reset to default."
template = get_setting("custom_template") or DEFAULT_TEMPLATE
name = get_site_name() name = get_site_name()
sharing = get_setting("sharing_enabled", "0") sharing = get_setting("sharing_enabled", "0")
checked = " checked" if sharing == "1" else "" checked = " checked" if sharing == "1" else ""
@ -379,35 +376,26 @@ def handle_style_form(msg=""):
f"<h2>sharing</h2>" f"<h2>sharing</h2>"
f'<label><input type="checkbox" name="sharing_enabled" value="1"{checked}>' f'<label><input type="checkbox" name="sharing_enabled" value="1"{checked}>'
f" share your site list publicly at /api/sites</label><br><br>" f" share your site list publicly at /api/sites</label><br><br>"
f"<h2>custom css</h2>" f"<h2>custom html</h2>"
f"<p>Some classes you can target:</p>" f"<p>Edit the full page template. Use <code>{esc('{{content}}')}</code> "
f"<pre>" f"where page content should appear.</p>"
f"body - page background, font\n" f'<textarea name="template" rows="20" cols="60">{esc(template)}</textarea><br><br>'
f"h1 - page titles\n"
f"input, button - search bar\n"
f"a - links\n"
f".result - each search result\n"
f".note - your notes on results\n"
f".trusted - trusted sites dropdown\n"
f"small - url text\n"
f"ul, li - browse page list"
f"</pre>"
f'<textarea name="css" rows="16" cols="60">{esc(css)}</textarea><br><br>'
f'<button type="submit">save</button>' f'<button type="submit">save</button>'
f"</form>" f"</form>"
f"<h2>bookmarklet</h2>" f"<h2>bookmarklet</h2>"
f"<p>Drag this link to your bookmarks bar. Click it on any page to index it instantly.</p>" f"<p>Drag this link to your bookmarks bar. Click it on any page to index it instantly.</p>"
f'<p><a href="javascript:void(fetch(\'http://localhost:8080/bookmark?url=\'+encodeURIComponent(location.href)).then(r=>r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}</a></p>' f'<p><a href="javascript:void(fetch(\'http://localhost:8080/bookmark?url=\'+encodeURIComponent(location.href)).then(r=>r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}</a></p>'
f"<p>{msg}</p>" f"<p>{msg}</p>"
f'<a href="/">back</a>' f'<a href="/">back</a>',
use_default=True,
) )
def handle_style_submit(body): def handle_style_submit(body):
css = body.get("css", [""])[0] template = body.get("template", [""])[0]
name = body.get("site_name", ["tinyweb"])[0].strip() name = body.get("site_name", ["tinyweb"])[0].strip()
sharing = "1" if body.get("sharing_enabled") else "0" sharing = "1" if body.get("sharing_enabled") else "0"
set_setting("custom_css", css) set_setting("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "")
set_setting("site_name", name or "tinyweb") set_setting("site_name", name or "tinyweb")
set_setting("sharing_enabled", sharing) set_setting("sharing_enabled", sharing)
return handle_style_form("Saved.") return handle_style_form("Saved.")
@ -755,6 +743,7 @@ def handle_subscription_autosync(sub_id):
def handle_subscription_delete(sub_id): def handle_subscription_delete(sub_id):
db = get_db() db = get_db()
db.execute("DELETE FROM remote_pages WHERE subscription_id = ?", (sub_id,))
db.execute("DELETE FROM subscriptions WHERE id = ?", (sub_id,)) db.execute("DELETE FROM subscriptions WHERE id = ?", (sub_id,))
db.commit() db.commit()
db.close() db.close()
@ -826,7 +815,7 @@ def dispatch_request(data):
elif path == "/bookmark": elif path == "/bookmark":
return handle_bookmark(query) return handle_bookmark(query)
elif path == "/style": elif path == "/style":
return handle_style_form() return handle_style_form(query=query)
elif path == "/about": elif path == "/about":
return handle_about() return handle_about()
elif path == "/export": elif path == "/export":

BIN
index.db

Binary file not shown.

View file

@ -15,7 +15,26 @@ def snippet(text, query, ctx=80):
return ("..." if start > 0 else "") + text[start:end] + ("..." if end < len(text) else "") return ("..." if start > 0 else "") + text[start:end] + ("..." if end < len(text) else "")
def wrap_page(body_html): DEFAULT_TEMPLATE = "<html>\n<head>\n</head>\n<body>\n{{content}}\n</body>\n</html>"
css = get_setting("custom_css")
style = f"<style>{css}</style>" if css else ""
return f"<html><head>{style}</head><body>{body_html}</body></html>" def _default_template():
name = esc(get_setting("site_name", "tinyweb"))
return (
"<html>\n<head>\n</head>\n<body>\n"
f'<p><b><a href="/">{name}</a></b>'
' | <a href="/">search</a> | <a href="/pages">browse</a>'
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a>'
' | <a href="/style">customize</a> | <a href="/about">about</a></p>\n'
"<hr>\n{{content}}\n</body>\n</html>"
)
def wrap_page(body_html, use_default=False):
if use_default:
template = _default_template()
else:
template = get_setting("custom_template") or _default_template()
if "{{content}}" not in template:
template = _default_template()
return template.replace("{{content}}", body_html)