From 4f12da98228cf0efda8275430d6b3b420bb297bd Mon Sep 17 00:00:00 2001 From: Banana Pancake <256630335+TheBananaPancake@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:19:32 +0100 Subject: [PATCH] appview: add theme drop-down persisted in database Fixes #364 Allows users to manually change theme, useful for cases like LibreWolf where it defaults to light-mode irrespective of system settings (with resistFingerprinting enabled). Stores theme as 'light', 'dark', or 'auto' in the database, and dynamically adds/removes the 'dark' class from the HTML. Signed-off-by: TheBananaPancake --- appview/db/db.go | 14 ++++++++ appview/db/theme.go | 32 +++++++++++++++++ appview/middleware/middleware.go | 6 ++++ appview/models/theme.go | 5 +++ appview/pages/pages.go | 16 +++++---- appview/pages/templates/layouts/base.html | 6 +++- .../templates/user/settings/profile.html | 17 +++++++++ appview/signup/signup.go | 1 + appview/state/login.go | 1 + appview/state/profile.go | 24 +++++++++++++ appview/state/router.go | 1 + input.css | 36 +++++++++---------- tailwind.config.js | 2 +- 13 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 appview/db/theme.go create mode 100644 appview/models/theme.go diff --git a/appview/db/db.go b/appview/db/db.go index 51357984..b344e09f 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -2445,6 +2445,20 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { return err }) + orm.RunMigration(conn, logger, "add-theme-preferences", func(tx *sql.Tx) error { + _, err := tx.Exec(` + create table if not exists theme_preferences ( + id integer primary key autoincrement, + user_did text not null unique, + theme text not null check (theme in ('auto', 'light', 'dark')), + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + create index if not exists idx_theme_preferences_user_did on theme_preferences(user_did); + `) + return err + }) + orm.RunMigration(conn, logger, "drop-label-ops-indexed", func(tx *sql.Tx) error { _, err := tx.Exec(`alter table label_ops drop column indexed`) return err diff --git a/appview/db/theme.go b/appview/db/theme.go new file mode 100644 index 00000000..de2ea37d --- /dev/null +++ b/appview/db/theme.go @@ -0,0 +1,32 @@ +package db + +import ( + "database/sql" + + "tangled.org/core/appview/models" +) + +func GetThemePreference(e Execer, did string) (models.ThemePreference, error) { + var theme string + err := e.QueryRow( + `select theme from theme_preferences where user_did = ?`, + did, + ).Scan(&theme) + if err == sql.ErrNoRows { + return models.ThemePreference(models.ThemeAuto), nil + } + if err != nil { + return "", err + } + return models.ThemePreference(theme), nil +} + +func UpsertThemePreference(e Execer, did string, theme models.ThemePreference) error { + _, err := e.Exec( + `insert into theme_preferences (user_did, theme, updated_at) values (?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + on conflict(user_did) do update set theme = excluded.theme, updated_at = excluded.updated_at`, + did, + string(theme), + ) + return err +} diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go index c89e46b3..4a1bb890 100644 --- a/appview/middleware/middleware.go +++ b/appview/middleware/middleware.go @@ -103,6 +103,12 @@ func (m *Middleware) InjectBaseParams(next http.Handler) http.Handler { LoggedInUser: user, } if user != nil { + theme, err := db.GetThemePreference(m.db, user.Did) + if err != nil { + slog.Default().Warn("failed to get theme preference", "err", err) + } else { + bp.ThemePreference = theme + } if focusing, _ := db.GetFocusStatus(m.db, user.Did); focusing { if item, _ := db.GetNextFocusItem(m.db, user.Did); item != nil { count, _ := db.CountFocusNotifs(m.db, user.Did) diff --git a/appview/models/theme.go b/appview/models/theme.go new file mode 100644 index 00000000..d50cf775 --- /dev/null +++ b/appview/models/theme.go @@ -0,0 +1,5 @@ +package models + +type ThemePreference string + +const ThemeAuto ThemePreference = "auto" diff --git a/appview/pages/pages.go b/appview/pages/pages.go index f59b8c4e..9b436bef 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -43,8 +43,9 @@ var Files embed.FS type baseParamsCtxKey struct{} type BaseParams struct { - LoggedInUser *oauth.MultiAccountUser - FocusParams FocusParams + LoggedInUser *oauth.MultiAccountUser + FocusParams FocusParams + ThemePreference models.ThemePreference } type FocusParams struct { @@ -351,6 +352,7 @@ func (p *Pages) Login(w io.Writer, params LoginParams) error { } type SignupParams struct { + BaseParams CloudflareSiteKey string EmailId string } @@ -360,7 +362,7 @@ func (p *Pages) Signup(w io.Writer, params SignupParams) error { } func (p *Pages) CompleteSignup(w io.Writer) error { - return p.executeLogin("user/completeSignup", w, nil) + return p.executeLogin("user/completeSignup", w, BaseParams{}) } type SignupSuccessParams struct { @@ -2105,17 +2107,17 @@ func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { } func (p *Pages) Error500(w io.Writer) error { - return p.execute("errors/500", w, nil) + return p.execute("errors/500", w, BaseParams{}) } func (p *Pages) Error404(w io.Writer) error { - return p.execute("errors/404", w, nil) + return p.execute("errors/404", w, BaseParams{}) } func (p *Pages) ErrorKnot404(w io.Writer) error { - return p.execute("errors/knot404", w, nil) + return p.execute("errors/knot404", w, BaseParams{}) } func (p *Pages) Error503(w io.Writer) error { - return p.execute("errors/503", w, nil) + return p.execute("errors/503", w, BaseParams{}) } diff --git a/appview/pages/templates/layouts/base.html b/appview/pages/templates/layouts/base.html index aff718d1..30424eb4 100644 --- a/appview/pages/templates/layouts/base.html +++ b/appview/pages/templates/layouts/base.html @@ -1,6 +1,6 @@ {{ define "layouts/base" }} - + @@ -40,6 +40,10 @@ +