Skip to content

Commit 4ca36e0

Browse files
wxiaoguanghiifong
authored andcommitted
1 parent 3d3ecd0 commit 4ca36e0

File tree

10 files changed

+184
-42
lines changed

10 files changed

+184
-42
lines changed

routers/web/user/setting/profile.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -338,13 +338,7 @@ func Repos(ctx *context.Context) {
338338
func Appearance(ctx *context.Context) {
339339
ctx.Data["Title"] = ctx.Tr("settings.appearance")
340340
ctx.Data["PageIsSettingsAppearance"] = true
341-
342-
allThemes := webtheme.GetAvailableThemes(ctx)
343-
if webtheme.IsThemeAvailable(ctx, setting.Config().UI.DefaultTheme.Value(ctx)) {
344-
allThemes = util.SliceRemoveAll(allThemes, setting.Config().UI.DefaultTheme.Value(ctx))
345-
allThemes = append([]string{setting.Config().UI.DefaultTheme.Value(ctx)}, allThemes...) // move the default theme to the top
346-
}
347-
ctx.Data["AllThemes"] = allThemes
341+
ctx.Data["AllThemes"] = webtheme.GetAvailableThemes()
348342
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
349343

350344
var hiddenCommentTypes *big.Int
@@ -374,7 +368,7 @@ func UpdateUIThemePost(ctx *context.Context) {
374368
return
375369
}
376370

377-
if !webtheme.IsThemeAvailable(ctx, form.Theme) {
371+
if !webtheme.IsThemeAvailable(form.Theme) {
378372
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
379373
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
380374
return

services/webtheme/webtheme.go

Lines changed: 120 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
package webtheme
55

66
import (
7-
"context"
7+
"regexp"
88
"sort"
99
"strings"
1010
"sync"
@@ -13,67 +13,154 @@ import (
1313
"code.gitea.io/gitea/modules/log"
1414
"code.gitea.io/gitea/modules/public"
1515
"code.gitea.io/gitea/modules/setting"
16+
"code.gitea.io/gitea/modules/util"
1617
)
1718

1819
var (
19-
availableThemes []string
20-
availableThemesSet container.Set[string]
21-
themeOnce sync.Once
20+
availableThemes []*ThemeMetaInfo
21+
availableThemeInternalNames container.Set[string]
22+
themeOnce sync.Once
2223
)
2324

24-
func initThemes(ctx context.Context) {
25+
const (
26+
fileNamePrefix = "theme-"
27+
fileNameSuffix = ".css"
28+
)
29+
30+
type ThemeMetaInfo struct {
31+
FileName string
32+
InternalName string
33+
DisplayName string
34+
}
35+
36+
func parseThemeMetaInfoToMap(cssContent string) map[string]string {
37+
/*
38+
The theme meta info is stored in the CSS file's variables of `gitea-theme-meta-info` element,
39+
which is a privately defined and is only used by backend to extract the meta info.
40+
Not using ":root" because it is difficult to parse various ":root" blocks when importing other files,
41+
it is difficult to control the overriding, and it's difficult to avoid user's customized overridden styles.
42+
*/
43+
metaInfoContent := cssContent
44+
if pos := strings.LastIndex(metaInfoContent, "gitea-theme-meta-info"); pos >= 0 {
45+
metaInfoContent = metaInfoContent[pos:]
46+
}
47+
48+
reMetaInfoItem := `
49+
(
50+
\s*(--[-\w]+)
51+
\s*:
52+
\s*(
53+
("(\\"|[^"])*")
54+
|('(\\'|[^'])*')
55+
|([^'";]+)
56+
)
57+
\s*;
58+
\s*
59+
)
60+
`
61+
reMetaInfoItem = strings.ReplaceAll(reMetaInfoItem, "\n", "")
62+
reMetaInfoBlock := `\bgitea-theme-meta-info\s*\{(` + reMetaInfoItem + `+)\}`
63+
re := regexp.MustCompile(reMetaInfoBlock)
64+
matchedMetaInfoBlock := re.FindAllStringSubmatch(metaInfoContent, -1)
65+
if len(matchedMetaInfoBlock) == 0 {
66+
return nil
67+
}
68+
re = regexp.MustCompile(strings.ReplaceAll(reMetaInfoItem, "\n", ""))
69+
matchedItems := re.FindAllStringSubmatch(matchedMetaInfoBlock[0][1], -1)
70+
m := map[string]string{}
71+
for _, item := range matchedItems {
72+
v := item[3]
73+
if strings.HasPrefix(v, `"`) {
74+
v = strings.TrimSuffix(strings.TrimPrefix(v, `"`), `"`)
75+
v = strings.ReplaceAll(v, `\"`, `"`)
76+
} else if strings.HasPrefix(v, `'`) {
77+
v = strings.TrimSuffix(strings.TrimPrefix(v, `'`), `'`)
78+
v = strings.ReplaceAll(v, `\'`, `'`)
79+
}
80+
m[item[2]] = v
81+
}
82+
return m
83+
}
84+
85+
func defaultThemeMetaInfoByFileName(fileName string) *ThemeMetaInfo {
86+
themeInfo := &ThemeMetaInfo{
87+
FileName: fileName,
88+
InternalName: strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix),
89+
}
90+
themeInfo.DisplayName = themeInfo.InternalName
91+
return themeInfo
92+
}
93+
94+
func defaultThemeMetaInfoByInternalName(fileName string) *ThemeMetaInfo {
95+
return defaultThemeMetaInfoByFileName(fileNamePrefix + fileName + fileNameSuffix)
96+
}
97+
98+
func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
99+
themeInfo := defaultThemeMetaInfoByFileName(fileName)
100+
m := parseThemeMetaInfoToMap(cssContent)
101+
if m == nil {
102+
return themeInfo
103+
}
104+
themeInfo.DisplayName = m["--theme-display-name"]
105+
return themeInfo
106+
}
107+
108+
func initThemes() {
25109
availableThemes = nil
26110
defer func() {
27-
availableThemesSet = container.SetOf(availableThemes...)
28-
if !availableThemesSet.Contains(setting.Config().UI.DefaultTheme.Value(ctx)) {
29-
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.Config().UI.DefaultTheme.Value(ctx))
111+
availableThemeInternalNames = container.Set[string]{}
112+
for _, theme := range availableThemes {
113+
availableThemeInternalNames.Add(theme.InternalName)
114+
}
115+
if !availableThemeInternalNames.Contains(setting.UI.DefaultTheme) {
116+
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
30117
}
31118
}()
32119
cssFiles, err := public.AssetFS().ListFiles("/assets/css")
33120
if err != nil {
34121
log.Error("Failed to list themes: %v", err)
35-
availableThemes = []string{setting.Config().UI.DefaultTheme.Value(ctx)}
122+
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
36123
return
37124
}
38-
var foundThemes []string
39-
for _, name := range cssFiles {
40-
name, ok := strings.CutPrefix(name, "theme-")
41-
if !ok {
42-
continue
43-
}
44-
name, ok = strings.CutSuffix(name, ".css")
45-
if !ok {
46-
continue
125+
var foundThemes []*ThemeMetaInfo
126+
for _, fileName := range cssFiles {
127+
if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
128+
content, err := public.AssetFS().ReadFile("/assets/css/" + fileName)
129+
if err != nil {
130+
log.Error("Failed to read theme file %q: %v", fileName, err)
131+
continue
132+
}
133+
foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
47134
}
48-
foundThemes = append(foundThemes, name)
49135
}
50-
if len(setting.Config().UI.Themes.Value(ctx)) > 0 {
51-
allowedThemes := container.SetOf(setting.Config().UI.Themes.Value(ctx)...)
136+
if len(setting.UI.Themes) > 0 {
137+
allowedThemes := container.SetOf(setting.UI.Themes...)
52138
for _, theme := range foundThemes {
53-
if allowedThemes.Contains(theme) {
139+
if allowedThemes.Contains(theme.InternalName) {
54140
availableThemes = append(availableThemes, theme)
55141
}
56142
}
57143
} else {
58144
availableThemes = foundThemes
59145
}
60-
sort.Strings(availableThemes)
146+
sort.Slice(availableThemes, func(i, j int) bool {
147+
if availableThemes[i].InternalName == setting.UI.DefaultTheme {
148+
return true
149+
}
150+
return availableThemes[i].DisplayName < availableThemes[j].DisplayName
151+
})
61152
if len(availableThemes) == 0 {
62153
setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme")
63-
availableThemes = []string{setting.Config().UI.DefaultTheme.Value(ctx)}
154+
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
64155
}
65156
}
66157

67-
func GetAvailableThemes(ctx context.Context) []string {
68-
themeOnce.Do(func() {
69-
initThemes(ctx)
70-
})
158+
func GetAvailableThemes() []*ThemeMetaInfo {
159+
themeOnce.Do(initThemes)
71160
return availableThemes
72161
}
73162

74-
func IsThemeAvailable(ctx context.Context, name string) bool {
75-
themeOnce.Do(func() {
76-
initThemes(ctx)
77-
})
78-
return availableThemesSet.Contains(name)
163+
func IsThemeAvailable(internalName string) bool {
164+
themeOnce.Do(initThemes)
165+
return availableThemeInternalNames.Contains(internalName)
79166
}

services/webtheme/webtheme_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package webtheme
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestParseThemeMetaInfo(t *testing.T) {
13+
m := parseThemeMetaInfoToMap(`gitea-theme-meta-info {
14+
--k1: "v1";
15+
--k2: "v\"2";
16+
--k3: 'v3';
17+
--k4: 'v\'4';
18+
--k5: v5;
19+
}`)
20+
assert.Equal(t, map[string]string{
21+
"--k1": "v1",
22+
"--k2": `v"2`,
23+
"--k3": "v3",
24+
"--k4": "v'4",
25+
"--k5": "v5",
26+
}, m)
27+
28+
// if an auto theme imports others, the meta info should be extracted from the last one
29+
// the meta in imported themes should be ignored to avoid incorrect overriding
30+
m = parseThemeMetaInfoToMap(`
31+
@media (prefers-color-scheme: dark) { gitea-theme-meta-info { --k1: foo; } }
32+
@media (prefers-color-scheme: light) { gitea-theme-meta-info { --k1: bar; } }
33+
gitea-theme-meta-info {
34+
--k2: real;
35+
}`)
36+
assert.Equal(t, map[string]string{"--k2": "real"}, m)
37+
}

templates/user/settings/appearance.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<label>{{ctx.Locale.Tr "settings.ui"}}</label>
1919
<select name="theme" class="ui dropdown">
2020
{{range $theme := .AllThemes}}
21-
<option value="{{$theme}}" {{Iif (eq ($.SignedUser.GetTheme ctx) $theme) "selected"}}>{{$theme}}</option>
21+
<option value="{{$theme.InternalName}}" {{Iif (eq $.SignedUser.Theme $theme.InternalName) "selected"}}>{{$theme.DisplayName}}</option>
2222
{{end}}
2323
</select>
2424
</div>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
@import "./theme-gitea-light-protanopia-deuteranopia.css" (prefers-color-scheme: light);
22
@import "./theme-gitea-dark-protanopia-deuteranopia.css" (prefers-color-scheme: dark);
3+
4+
gitea-theme-meta-info {
5+
--theme-display-name: "Auto (Red/Green Colorblind-friendly)";
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
@import "./theme-gitea-light.css" (prefers-color-scheme: light);
22
@import "./theme-gitea-dark.css" (prefers-color-scheme: dark);
3+
4+
gitea-theme-meta-info {
5+
--theme-display-name: "Auto";
6+
}

web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
@import "./theme-gitea-dark.css";
22

3+
gitea-theme-meta-info {
4+
--theme-display-name: "Dark (Red/Green Colorblind-friendly)";
5+
}
6+
37
/* red/green colorblind-friendly colors */
48
/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */
59
:root {

web_src/css/themes/theme-gitea-dark.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
@import "../chroma/dark.css";
22
@import "../codemirror/dark.css";
33

4+
gitea-theme-meta-info {
5+
--theme-display-name: "Dark";
6+
}
7+
48
:root {
59
--is-dark-theme: true;
610
--color-primary: #4183c4;

web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
@import "./theme-gitea-light.css";
22

3+
gitea-theme-meta-info {
4+
--theme-display-name: "Light (Red/Green Colorblind-friendly)";
5+
}
6+
37
/* red/green colorblind-friendly colors */
48
/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */
59
:root {

web_src/css/themes/theme-gitea-light.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
@import "../chroma/light.css";
22
@import "../codemirror/light.css";
33

4+
gitea-theme-meta-info {
5+
--theme-display-name: "Light";
6+
}
7+
48
:root {
59
--is-dark-theme: false;
610
--color-primary: #4183c4;

0 commit comments

Comments
 (0)