Add theme info
Some checks are pending
/ release (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions

Display is rough right now but better than nothing. Icons especially to be improved.
This commit is contained in:
Minecon724 2025-05-10 16:45:25 +00:00
commit 7556843ec1
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
19 changed files with 231 additions and 96 deletions

View file

@ -24,7 +24,6 @@ Don't be overwhelmed, commits are very granular... except when they aren't.
- [*Fix forgejo/forgejo#7250*](/git724/forgejo/commit/6eeb0009ef574900e5f11936bc783acf561d8825): Fixes [forgejo/forgejo#7250](https://codeberg.org/forgejo/forgejo/issues/7250) - [*Fix forgejo/forgejo#7250*](/git724/forgejo/commit/6eeb0009ef574900e5f11936bc783acf561d8825): Fixes [forgejo/forgejo#7250](https://codeberg.org/forgejo/forgejo/issues/7250)
- [*Fix issue popup for non-JSON responses*](/git724/forgejo/commit/22a74730d36ec13cd098a2a0e3e4f294160580ca): Basically, if you hover on a issue #, previously it displayed a popup with a "Network error" because it expects messages to be JSON. Now it can parse both. - [*Fix issue popup for non-JSON responses*](/git724/forgejo/commit/22a74730d36ec13cd098a2a0e3e4f294160580ca): Basically, if you hover on a issue #, previously it displayed a popup with a "Network error" because it expects messages to be JSON. Now it can parse both.
- [*Issue popup message that the issue doesn't exist*](/git724/forgejo/commit/6f2a441ed52722337137eb16f157ca076e3983be): Adds an "Issue not found" message that replaces generic 404. - [*Issue popup message that the issue doesn't exist*](/git724/forgejo/commit/6f2a441ed52722337137eb16f157ca076e3983be): Adds an "Issue not found" message that replaces generic 404.
- [*Theme picker warning with hardcoded link*](/git724/forgejo/commit/a94e53c01722fa03a16e23e56de5e72c983ff228): Adds a warning above theme picker that third-party themes are not as reliable and credits. [**Points to here, this is hardcoded.**](https://git.m724.eu/git724/git724/src/branch/master/THEMES.md)
- [*Move user RSS icon (WIP)*](/git724/forgejo/commit/c17f3738377c0939d9f95535990ce05482b2e9d3): Moves RSS link to *Public activity*, because the RSS feed is public activity. I don't remember why WIP. - [*Move user RSS icon (WIP)*](/git724/forgejo/commit/c17f3738377c0939d9f95535990ce05482b2e9d3): Moves RSS link to *Public activity*, because the RSS feed is public activity. I don't remember why WIP.
- [*Center padlock icon on profile page*](/git724/forgejo/commit/8ce85c11fd0122a7ff5274d982d141637802aa39): Centers vertically *Email privacy* link icon on user profile page. - [*Center padlock icon on profile page*](/git724/forgejo/commit/8ce85c11fd0122a7ff5274d982d141637802aa39): Centers vertically *Email privacy* link icon on user profile page.
- [*Fix footer link margin*](/git724/forgejo/commit/cbdce79d8b2f878e0635ba513a8a27ead59d9888) - [*Fix footer link margin*](/git724/forgejo/commit/cbdce79d8b2f878e0635ba513a8a27ead59d9888)
@ -38,9 +37,8 @@ Don't be overwhelmed, commits are very granular... except when they aren't.
- [*Remove hover transition from buttons*](/git724/forgejo/commit/d965f0080289eda053461bc0a61c643abf11756e) - [*Remove hover transition from buttons*](/git724/forgejo/commit/d965f0080289eda053461bc0a61c643abf11756e)
- [*Remove "API" from footer*](/git724/forgejo/commit/5979129aa6acde0422654fd6ec0f4efd862eb975) - [*Remove "API" from footer*](/git724/forgejo/commit/5979129aa6acde0422654fd6ec0f4efd862eb975)
- [*Dynamic theme loading*](/git724/forgejo/commit/d71c372080e4008551db79f4d92a2fbdfcde19cc): Loads themes from a directory on startup. Normally you'd list every theme variant in your config, which can be inconvenient, especially with a lot of themes. - [*Dynamic theme loading*](/git724/forgejo/commit/d71c372080e4008551db79f4d92a2fbdfcde19cc): Loads themes from a directory on startup. Normally you'd list every theme variant in your config, which can be inconvenient, especially with a lot of themes.
- [*Improve theme picker*](/git724/forgejo/commit/9542895e03e487cfd0a7c673257cc383295b5d76): Makes the theme picker searchable and changes `names-like-this` to `Names Like This`. - [*Improve theme picker*](/git724/forgejo/commit/9542895e03e487cfd0a7c673257cc383295b5d76): Makes the theme picker searchable and changes `names-like-this` to `Names Like This`. (precedes the below)
- [*Add theme info*](): Adds theme info (url, author, desc, etc.), see [the stock themes](/git724/forgejo/src/branch/v11.0/forgejo/web_src/css/themes). Also tweaks `make watch` so webpack completes first. Display is to be improved.
You can see [all my changes here](/git724/forgejo/commits/branch/v11.0/forgejo/search?q=Minecon724&all=). \
--- ---

View file

@ -22,6 +22,7 @@ import (
"forgejo.org/modules/process" "forgejo.org/modules/process"
"forgejo.org/modules/public" "forgejo.org/modules/public"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/theme"
"forgejo.org/routers" "forgejo.org/routers"
"forgejo.org/routers/install" "forgejo.org/routers/install"
@ -218,6 +219,8 @@ func serveInstalled(ctx *cli.Context) error {
} }
} }
theme.GetThemes()
// Set up Chi routes // Set up Chi routes
webRoutes := routers.NormalRoutes() webRoutes := routers.NormalRoutes()
err := listen(webRoutes, true) err := listen(webRoutes, true)

View file

@ -1,7 +1,12 @@
package theme package theme
import ( import (
"bufio"
"errors"
"fmt" "fmt"
"io"
"maps"
"slices"
"strings" "strings"
"forgejo.org/modules/assetfs" "forgejo.org/modules/assetfs"
@ -12,77 +17,96 @@ import (
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
const (
key string = "load-themes"
)
var ( var (
group singleflight.Group group singleflight.Group
assetFs *assetfs.LayeredFS assetFs *assetfs.LayeredFS
loaded bool loaded bool
themes map[string]Theme
) )
// LoadThemes loads installed themes type Theme struct {
ID string
Name string
Family string
Author string
Url string
Description string
Scheme string
Verified bool
}
// GetThemes gets installed Themes
// //
// Note that you can't just run this during init, because webpack mightn't have completed yet. // Note that you can't just run this during init, because webpack mightn't have completed yet.
// Hence, we're loading on first demand. // Hence, we're loading on first demand.
func LoadThemes() error { func GetThemes() (map[string]Theme, error) {
if loaded { if loaded {
return nil return themes, nil
} }
if assetFs == nil { if assetFs == nil {
assetFs = public.AssetFS() assetFs = public.AssetFS()
} }
_, err, _ := group.Do(key, func() (interface{}, error) { _, err, _ := group.Do("load-themes", func() (any, error) {
themes, err := loadThemesInner(assetFs) _themes, err := loadThemesInner(assetFs)
log.Info("Loaded %d themes", len(_themes))
if err != nil { if err != nil {
group.Forget(key) group.Forget("load-themes")
} else { } else {
setting.UI.Themes = themes setting.UI.Themes = slices.Collect(maps.Keys(_themes))
themes = _themes
loaded = true loaded = true
} }
return nil, err return nil, err
}) })
return err return themes, err
} }
func loadThemesInner(assetFs *assetfs.LayeredFS) ([]string, error) { func loadThemesInner(assetFs *assetfs.LayeredFS) (map[string]Theme, error) {
entries, err := assetFs.ListFiles("assets/css") entries, err := assetFs.ListFiles("assets/css")
if err != nil { if err != nil {
return nil, err return nil, err
} }
var themes []string themes := make(map[string]Theme)
for _, entry := range entries { for _, entry := range entries {
if !(strings.HasPrefix(entry, "theme-") && strings.HasSuffix(entry, ".css")) { if !(strings.HasPrefix(entry, "theme-") && strings.HasSuffix(entry, ".css")) {
continue continue
} }
theme := entry[6 : len(entry)-4] id := entry[6 : len(entry)-4]
themes = append(themes, theme) theme := Theme{
ID: id,
log.Debug("Found theme: %s", theme) Name: getFriendlyThemeName(id),
} }
if len(themes) > 0 { if err := loadThemeMeta(assetFs, entry, &theme); err != nil {
log.Info("Loaded %d themes", len(themes)) log.Warn("Failed to load meta of theme %s: %s", id, err)
return themes, nil }
} else {
themes[id] = theme
log.Debug("Found theme: %s (%s)", theme.Name, theme.ID)
}
if len(themes) == 0 {
return nil, fmt.Errorf("no themes found") return nil, fmt.Errorf("no themes found")
} }
return themes, nil
} }
// GetFriendlyThemeName converts an raw theme name (forgejo-dark) to a friendly name (Forgejo Dark) // getFriendlyThemeName converts an raw theme name (forgejo-dark) to a friendly name (Forgejo Dark)
// //
// Example: forgejo-dark -> Forgejo Dark, catppuccin-maroon-auto -> Catppuccin Maroon Auto // Example: forgejo-dark -> Forgejo Dark, catppuccin-maroon-auto -> Catppuccin Maroon Auto
func GetFriendlyThemeName(themeName string) string { func getFriendlyThemeName(themeName string) string {
themeName = strings.ReplaceAll(themeName, "-", " ") themeName = strings.ReplaceAll(themeName, "-", " ")
themeName = strings.ToLower(themeName) themeName = strings.ToLower(themeName)
@ -90,3 +114,76 @@ func GetFriendlyThemeName(themeName string) string {
return themeName return themeName
} }
func loadThemeMeta(assetFs *assetfs.LayeredFS, filename string, theme *Theme) error {
file, err := assetFs.Open("assets/css/" + filename)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
kv, err := scanTheme(file, filename[6:len(filename)-4])
if err != nil {
return fmt.Errorf("error scanning theme for meta: %w", err)
}
theme.Name = kv["name"]
theme.Family = kv["family"]
theme.Author = kv["author"]
theme.Url = kv["url"]
theme.Description = kv["description"]
theme.Scheme = strings.ToLower(kv["scheme"])
theme.Verified = kv["verified"] == "yes"
return nil
}
func scanTheme(file io.Reader, id string) (map[string]string, error) {
var kv = make(map[string]string)
scanner := bufio.NewScanner(file)
ok := false
// webpack adds headers
for scanner.Scan() {
if strings.TrimSpace(scanner.Text()) == "/* theme "+id {
ok = true
break
}
}
if !ok {
return nil, errors.New("theme has no meta")
}
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) == 0 {
continue
}
key := strings.ToLower(parts[0])
if key == "*/" {
break
}
if len(parts) == 1 {
kv[key] = "yes"
} else {
kv[key] = strings.Join(parts[1:], " ")
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scanner error: %w", err)
}
return kv, nil
}

View file

@ -835,7 +835,6 @@ manage_themes = Default theme
manage_openid = OpenID addresses manage_openid = OpenID addresses
email_desc = Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations. email_desc = Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations.
theme_desc = This theme will be used for the web interface when you are logged in. theme_desc = This theme will be used for the web interface when you are logged in.
theme_warning = <strong>WARNING:</strong> Themes are made by third parties. <a href="/git724/git724/src/branch/master/THEMES.md">Click to read more.</a>
primary = Primary primary = Primary
activated = Activated activated = Activated
requires_activation = Requires activation requires_activation = Requires activation
@ -849,6 +848,10 @@ email_deletion_desc = The email address and related information will be removed
email_deletion_success = The email address has been removed. email_deletion_success = The email address has been removed.
theme_update_success = Your theme was updated. theme_update_success = Your theme was updated.
theme_update_error = The selected theme does not exist. theme_update_error = The selected theme does not exist.
theme_auto = Dynamic / Synchronizes with your system
theme_light = Light
theme_dark = Dark
theme_verified = Verified
openid_deletion = Remove OpenID Address openid_deletion = Remove OpenID Address
openid_deletion_desc = Removing this OpenID address from your account will prevent you from signing in with it. Continue? openid_deletion_desc = Removing this OpenID address from your account will prevent you from signing in with it. Continue?
openid_deletion_success = The OpenID address has been removed. openid_deletion_success = The OpenID address has been removed.

View file

@ -328,29 +328,16 @@ func Repos(ctx *context.Context) {
// Appearance render user's appearance settings // Appearance render user's appearance settings
func Appearance(ctx *context.Context) { func Appearance(ctx *context.Context) {
err := theme.LoadThemes() themes, err := theme.GetThemes()
if err != nil { if err != nil {
ctx.ServerError("Failed to load themes", err) ctx.ServerError("Failed to load themes", err)
return return
} }
ctx.Data["AllThemes"] = setting.UI.Themes
ctx.Data["Title"] = ctx.Tr("settings.appearance") ctx.Data["Title"] = ctx.Tr("settings.appearance")
ctx.Data["PageIsSettingsAppearance"] = true ctx.Data["PageIsSettingsAppearance"] = true
ctx.Data["AllThemes"] = setting.UI.Themes ctx.Data["AllThemes"] = themes
ctx.Data["ThemeName"] = func(themeName string) string {
fullThemeName := "themes.names." + themeName
if ctx.Locale.HasKey(fullThemeName) {
return ctx.Locale.TrString(fullThemeName)
}
// We should probably cache, because this does some operations on the string
themeName = theme.GetFriendlyThemeName(themeName)
return themeName
}
var hiddenCommentTypes *big.Int var hiddenCommentTypes *big.Int
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)

View file

@ -7,11 +7,27 @@
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
<div class="ui email list"> <div class="ui email list">
<div class="item tw-mb-4"> {{$theme := index .AllThemes $.SignedUser.Theme}}
<p>{{ctx.Locale.Tr "settings.theme_desc"}}</p> {{if $theme.Author}}
<p>{{ctx.Locale.Tr "settings.theme_warning"}}</p> <div class="item">
<p>
<strong>{{$theme.Name}}</strong>
by <em><a href="{{$theme.Url}}">{{$theme.Author}}</a></em> {{if $theme.Verified}}<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_verified"}}">{{svg "octicon-verified" 16 "dropdown icon"}}</span>{{end}}
</p>
{{if $theme.Description}}
<p><em>"{{$theme.Description}}"</em></p>
{{end}}
</div> </div>
<div class="item tw-mb-auto">
{{ctx.Locale.Tr "settings.theme_desc"}}
</div>
{{else}}
<div class="item tw-mb-4">
{{ctx.Locale.Tr "settings.theme_desc"}}
</div>
{{end}}
<form class="ui form" action="{{.Link}}/theme" method="post"> <form class="ui form" action="{{.Link}}/theme" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<div class="field"> <div class="field">
@ -20,14 +36,24 @@
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="text"> <div class="text">
{{- range $i,$a := .AllThemes -}} {{- range $i,$a := .AllThemes -}}
{{if eq $.SignedUser.Theme $a}}{{call $.ThemeName $a}}{{end}} {{if eq $.SignedUser.Theme $i}}{{$a.Name}}{{end}}
{{- end -}} {{- end -}}
</div> </div>
<div class="menu"> <div class="menu">
{{range $i,$a := .AllThemes}} {{range $i,$a := .AllThemes}}
<div class="item{{if eq $.SignedUser.Theme $a}} active selected{{end}}" data-value="{{$a}}"> <div class="item{{if eq $.SignedUser.Theme $i}} active selected{{end}} tw-flex" data-value="{{$i}}">
{{call $.ThemeName $a}} {{$a.Name}}
{{if $a.Verified}}
<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_verified"}}">{{svg "octicon-verified" 16 "dropdown icon"}}</span>
{{end}}
{{if eq $a.Scheme "auto"}}
<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_auto"}}">{{svg "octicon-sync" 16 "dropdown icon"}}</span>
{{else if eq $a.Scheme "dark"}}
<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_dark"}}">{{svg "octicon-moon" 16 "dropdown icon"}}</span>
{{else if eq $a.Scheme "light"}}
<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_light"}}">{{svg "octicon-sun" 16 "dropdown icon"}}</span>
{{end}}
</div> </div>
{{end}} {{end}}
</div> </div>

View file

@ -1,8 +1,13 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
make --no-print-directory watch-frontend & make --no-print-directory watch-frontend | tee >(
make --no-print-directory watch-backend & awk -v pattern="webpack" '
$0 ~ pattern {
system("make --no-print-directory watch-backend &")
}
'
) &
trap 'kill $(jobs -p)' EXIT trap 'kill $(jobs -p)' EXIT
wait wait

View file

@ -1,2 +0,0 @@
@import "theme-forgejo-light-deuteranopia-protanopia.css";
@import "theme-forgejo-dark-deuteranopia-protanopia.css" (prefers-color-scheme: dark);

View file

@ -1,2 +0,0 @@
@import "theme-forgejo-light-tritanopia.css";
@import "theme-forgejo-dark-tritanopia.css" (prefers-color-scheme: dark);

View file

@ -1,2 +1,12 @@
/* theme forgejo-auto
Name Forgejo
Family Forgejo
Author Forgejo authors
Url https://forgejo.org/
Description The fresh of freedom.
Scheme Auto
Verified
*/
@import "theme-forgejo-light.css"; @import "theme-forgejo-light.css";
@import "theme-forgejo-dark.css" (prefers-color-scheme: dark); @import "theme-forgejo-dark.css" (prefers-color-scheme: dark);

View file

@ -1,9 +0,0 @@
@import "./theme-forgejo-dark.css";
:root {
/* removed rows/words: use red colors from vanilla forgejo-dark */
--color-diff-added-word-bg: #214d88;
--color-diff-added-row-border: #214d88;
--color-diff-added-row-bg: #18184f;
--color-code-bg: #0d1117;
}

View file

@ -1,9 +0,0 @@
@import "./theme-forgejo-dark.css";
:root {
/* removed rows/words: use red colors from vanilla forgejo-dark */
--color-diff-added-word-bg: #214d88;
--color-diff-added-row-border: #214d88;
--color-diff-added-row-bg: #152846;
--color-code-bg: #0d1117;
}

View file

@ -1,3 +1,13 @@
/* theme forgejo-dark
Name Forgejo
Family Forgejo
Author Forgejo authors
Url https://forgejo.org/
Description The fresh of freedom.
Scheme Dark
Verified
*/
@import "../chroma/dark.css"; @import "../chroma/dark.css";
@import "../codemirror/dark.css"; @import "../codemirror/dark.css";
@import "../markup/dark.css"; @import "../markup/dark.css";

View file

@ -1,11 +0,0 @@
@import "./theme-forgejo-light.css";
:root {
--color-diff-removed-word-bg: #c8c850;
--color-diff-removed-row-border: #c8c850;
--color-diff-removed-row-bg: #ffecc4;
--color-diff-added-word-bg: #b8c0ff;
--color-diff-added-row-border: #b8c0ff;
--color-diff-added-row-bg: #e0e0ff;
--color-code-bg: #ffffff;
}

View file

@ -1,11 +0,0 @@
@import "./theme-forgejo-light.css";
:root {
--color-diff-removed-word-bg: #ffb8c0;
--color-diff-removed-row-border: #ffb8c0;
--color-diff-removed-row-bg: #ffd8d8;
--color-diff-added-word-bg: #b8c0ff;
--color-diff-added-row-border: #b8c0ff;
--color-diff-added-row-bg: #c0e8ff;
--color-code-bg: #ffffff;
}

View file

@ -1,3 +1,13 @@
/* theme forgejo-light
Name Forgejo
Family Forgejo
Author Forgejo authors
Url https://forgejo.org/
Description The fresh of freedom.
Scheme Light
Verified
*/
@import "../chroma/light.css"; @import "../chroma/light.css";
@import "../codemirror/light.css"; @import "../codemirror/light.css";
@import "../markup/light.css"; @import "../markup/light.css";

View file

@ -1,2 +1,12 @@
/* theme gitea-auto
Name Gitea
Family Gitea
Author The Gitea Authors
Url https://github.com/go-gitea/gitea
Description The OG look.
Scheme Auto
Verified
*/
@import "./theme-gitea-light.css" (prefers-color-scheme: light); @import "./theme-gitea-light.css" (prefers-color-scheme: light);
@import "./theme-gitea-dark.css" (prefers-color-scheme: dark); @import "./theme-gitea-dark.css" (prefers-color-scheme: dark);

View file

@ -1,3 +1,13 @@
/* theme gitea-dark
Name Gitea
Family Gitea
Author The Gitea Authors
Url https://github.com/go-gitea/gitea
Description The OG look.
Scheme Dark
Verified
*/
@import "../chroma/dark.css"; @import "../chroma/dark.css";
@import "../codemirror/dark.css"; @import "../codemirror/dark.css";
@import "../markup/dark.css"; @import "../markup/dark.css";

View file

@ -1,3 +1,13 @@
/* theme gitea-light
Name Gitea
Family Gitea
Author The Gitea Authors
Url https://github.com/go-gitea/gitea
Description The OG look.
Scheme Light
Verified
*/
@import "../chroma/light.css"; @import "../chroma/light.css";
@import "../codemirror/light.css"; @import "../codemirror/light.css";
@import "../markup/light.css"; @import "../markup/light.css";