From db36f75519b2295c184d107f29210141bdd01082 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 9 Feb 2026 20:23:31 +0100 Subject: [PATCH] Support restoring preferences via new prefs param Fixes #1352 Fixes #553 Fixes #249 --- src/nitter.nim | 5 +++++ src/prefs.nim | 9 +++++++-- src/prefs_impl.nim | 30 ++++++++++++++++++++++++++++++ src/routes/preferences.nim | 4 +++- src/routes/router_utils.nim | 32 +++++++++++++++++++++++++++++--- src/sass/index.scss | 18 ++++++++++++------ src/sass/inputs.scss | 12 ++++++++++++ src/utils.nim | 2 +- src/views/preferences.nim | 9 ++++++++- 9 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/nitter.nim b/src/nitter.nim index 9e20ecb..442a8c0 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -65,6 +65,11 @@ settings: reusePort = true routes: + before: + # skip all file URLs + cond "." notin request.path + applyUrlPrefs() + get "/": resp renderMain(renderSearch(), request, cfg, cookiePrefs()) diff --git a/src/prefs.nim b/src/prefs.nim index fa40a6d..573ccae 100644 --- a/src/prefs.nim +++ b/src/prefs.nim @@ -1,10 +1,10 @@ # SPDX-License-Identifier: AGPL-3.0-only -import tables +import tables, strutils, base64 import types, prefs_impl from config import get from parsecfg import nil -export genUpdatePrefs, genResetPrefs +export genUpdatePrefs, genResetPrefs, genApplyPrefs var defaultPrefs*: Prefs @@ -20,3 +20,8 @@ template getPref*(cookies: Table[string, string], pref): untyped = var res = defaultPrefs.`pref` genCookiePref(cookies, pref, res) res + +proc encodePrefs*(prefs: Prefs): string = + var encPairs: seq[string] + genEncodePrefs(prefs) + encode(encPairs.join("&"), safe=true) diff --git a/src/prefs_impl.nim b/src/prefs_impl.nim index e55c2b8..2faf8ef 100644 --- a/src/prefs_impl.nim +++ b/src/prefs_impl.nim @@ -205,6 +205,36 @@ macro genResetPrefs*(): untyped = result.add quote do: savePref(`name`, "", `req`, expire=true) +macro genEncodePrefs*(prefs): untyped = + result = nnkStmtList.newTree() + for pref in allPrefs(): + let + name = newLit(pref.name) + ident = ident(pref.name) + kind = newLit(pref.kind) + defaultIdent = nnkDotExpr.newTree(ident("defaultPrefs"), ident(pref.name)) + + result.add quote do: + when `kind` == checkbox: + if `prefs`.`ident` != `defaultIdent`: + if `prefs`.`ident`: + encPairs.add `name` & "=on" + else: + encPairs.add `name` & "=" + else: + if `prefs`.`ident` != `defaultIdent`: + encPairs.add `name` & "=" & `prefs`.`ident` + +macro genApplyPrefs*(params, req): untyped = + result = nnkStmtList.newTree() + for pref in allPrefs(): + let name = newLit(pref.name) + result.add quote do: + if `name` in `params`: + savePref(`name`, `params`[`name`], `req`) + else: + savePref(`name`, "", `req`, expire=true) + macro genPrefsType*(): untyped = let name = nnkPostfix.newTree(ident("*"), ident("Prefs")) result = quote do: diff --git a/src/routes/preferences.nim b/src/routes/preferences.nim index b8af03d..345ff34 100644 --- a/src/routes/preferences.nim +++ b/src/routes/preferences.nim @@ -20,7 +20,9 @@ proc createPrefRouter*(cfg: Config) = get "/settings": let prefs = cookiePrefs() - html = renderPreferences(prefs, refPath(), findThemes(cfg.staticDir)) + prefsCode = encodePrefs(prefs) + prefsUrl = getUrlPrefix(cfg) & "/?prefs=" & prefsCode + html = renderPreferences(prefs, refPath(), findThemes(cfg.staticDir), prefsUrl) resp renderMain(html, request, cfg, prefs, "Preferences") get "/settings/@i?": diff --git a/src/routes/router_utils.nim b/src/routes/router_utils.nim index 34fd163..2ef248a 100644 --- a/src/routes/router_utils.nim +++ b/src/routes/router_utils.nim @@ -1,15 +1,15 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, sequtils, uri, tables, json +import strutils, sequtils, uri, tables, json, base64 from jester import Request, cookies import ../views/general import ".."/[utils, prefs, types] -export utils, prefs, types, uri +export utils, prefs, types, uri, base64 template savePref*(pref, value: string; req: Request; expire=false) = if not expire or pref in cookies(req): setCookie(pref, value, daysForward(when expire: -10 else: 360), - httpOnly=true, secure=cfg.useHttps, sameSite=None) + httpOnly=true, secure=cfg.useHttps, sameSite=None, path="/") template cookiePrefs*(): untyped {.dirty.} = getPrefs(cookies(request)) @@ -38,5 +38,31 @@ template getCursor*(req: Request): string = proc getNames*(name: string): seq[string] = name.strip(chars={'/'}).split(",").filterIt(it.len > 0) +template applyUrlPrefs*() {.dirty.} = + if @"prefs".len > 0: + try: + let decoded = decode(@"prefs") + var params = initTable[string, string]() + for pair in decoded.split('&'): + let kv = pair.split('=', maxsplit=1) + if kv.len == 2: + params[kv[0]] = kv[1] + elif kv.len == 1 and kv[0].len > 0: + params[kv[0]] = "" + genApplyPrefs(params, request) + except: discard + + # Rebuild URL without prefs param + var params: seq[(string, string)] + for k, v in request.params: + if k != "prefs": + params.add (k, v) + + if params.len > 0: + let cleanUrl = request.getNativeReq.url ? params + redirect($cleanUrl) + else: + redirect(request.path) + template respJson*(node: JsonNode) = resp $node, "application/json" diff --git a/src/sass/index.scss b/src/sass/index.scss index c12b814..4ca4f3d 100644 --- a/src/sass/index.scss +++ b/src/sass/index.scss @@ -99,12 +99,18 @@ legend { margin-bottom: 8px; } -.preferences .note { - border-top: 1px solid var(--border_grey); - border-bottom: 1px solid var(--border_grey); - padding: 6px 0 8px 0; - margin-bottom: 8px; - margin-top: 16px; +.preferences { + .note { + border-top: 1px solid var(--border_grey); + border-bottom: 1px solid var(--border_grey); + padding: 6px 0 8px 0; + margin-bottom: 8px; + margin-top: 16px; + } + + .bookmark-note { + margin: 0; + } } ul { diff --git a/src/sass/inputs.scss b/src/sass/inputs.scss index aafa5b8..7ea2b0a 100644 --- a/src/sass/inputs.scss +++ b/src/sass/inputs.scss @@ -200,4 +200,16 @@ input::-webkit-datetime-edit-year-field:focus { .pref-reset { float: left; } + + .prefs-code { + background-color: var(--bg_elements); + border: 1px solid var(--accent_border); + color: var(--fg_color); + font-size: 12px; + padding: 6px 8px; + margin: 4px 0; + word-break: break-all; + white-space: pre-wrap; + user-select: all; + } } diff --git a/src/utils.nim b/src/utils.nim index c96a6dd..667299c 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -9,7 +9,7 @@ var const https* = "https://" twimg* = "pbs.twimg.com/" - nitterParams = ["name", "tab", "id", "list", "referer", "scroll"] + nitterParams* = ["name", "tab", "id", "list", "referer", "scroll", "prefs"] twitterDomains = @[ "twitter.com", "pic.twitter.com", diff --git a/src/views/preferences.nim b/src/views/preferences.nim index 1787704..40e9e11 100644 --- a/src/views/preferences.nim +++ b/src/views/preferences.nim @@ -32,7 +32,8 @@ macro renderPrefs*(): untyped = result[2].add stmt -proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]): VNode = +proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]; + prefsUrl: string): VNode = buildHtml(tdiv(class="overlay-panel")): fieldset(class="preferences"): form(`method`="post", action="/saveprefs", autocomplete="off"): @@ -40,6 +41,12 @@ proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]): VNode renderPrefs() + legend: text "Bookmark" + p(class="bookmark-note"): + text "Save this URL to restore your preferences (?prefs works on all pages)" + pre(class="prefs-code"): + text prefsUrl + h4(class="note"): text "Preferences are stored client-side using cookies without any personal information."