Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve theme display #30671

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 1 addition & 7 deletions routers/web/user/setting/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,7 @@ func Repos(ctx *context.Context) {
func Appearance(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.appearance")
ctx.Data["PageIsSettingsAppearance"] = true

allThemes := webtheme.GetAvailableThemes()
if webtheme.IsThemeAvailable(setting.UI.DefaultTheme) {
allThemes = util.SliceRemoveAll(allThemes, setting.UI.DefaultTheme)
allThemes = append([]string{setting.UI.DefaultTheme}, allThemes...) // move the default theme to the top
}
ctx.Data["AllThemes"] = allThemes
ctx.Data["AllThemes"] = webtheme.GetAvailableThemes()

var hiddenCommentTypes *big.Int
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
Expand Down
138 changes: 116 additions & 22 deletions services/webtheme/webtheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package webtheme

import (
"regexp"
"sort"
"strings"
"sync"
Expand All @@ -12,63 +13,156 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)

var (
availableThemes []string
availableThemesSet container.Set[string]
themeOnce sync.Once
availableThemes []*ThemeMetaInfo
availableThemeInternalNames container.Set[string]
themeOnce sync.Once
)

const (
fileNamePrefix = "theme-"
fileNameSuffix = ".css"
)

type ThemeMetaInfo struct {
FileName string
InternalName string
DisplayName string
PreferColorSchemes container.Set[string]
}

func parseThemeMetaInfoToMap(cssContent string) map[string]string {
metaInfoContent := cssContent
if pos := strings.LastIndex(metaInfoContent, "gitea-theme-meta-info"); pos >= 0 {
metaInfoContent = metaInfoContent[pos:]
}

reMetaInfoItem := `
(
\s*(--[-\w]+)
\s*:
\s*("(\\"|[^"])*")
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
\s*;
\s*
)
`
reMetaInfoItem = strings.ReplaceAll(reMetaInfoItem, "\n", "")
reMetaInfoBlock := `\bgitea-theme-meta-info\s*\{(` + reMetaInfoItem + `+)\}`
re := regexp.MustCompile(reMetaInfoBlock)
matchedMetaInfoBlock := re.FindAllStringSubmatch(metaInfoContent, -1)
if len(matchedMetaInfoBlock) == 0 {
return nil
}
re = regexp.MustCompile(strings.ReplaceAll(reMetaInfoItem, "\n", ""))
matchedItems := re.FindAllStringSubmatch(matchedMetaInfoBlock[0][1], -1)
m := map[string]string{}
for _, item := range matchedItems {
v := item[3]
v = strings.TrimPrefix(v, "\"")
v = strings.TrimSuffix(v, "\"")
v = strings.ReplaceAll(v, `\"`, `"`)
m[item[2]] = v
}
return m
}
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved

// @media (prefers-color-scheme: dark)
func parseThemePreferColorSchemes(cssContent string) container.Set[string] {
re := regexp.MustCompile(`@media\s*\(\s*prefers-color-scheme\s*:\s*([-\w]+)\s*\)`)
matched := re.FindAllStringSubmatch(cssContent, -1)
if len(matched) == 0 {
return nil
}
schemes := container.Set[string]{}
for _, m := range matched {
schemes.Add(m[1])
}
return schemes
}

func defaultThemeMetaInfoByFileName(fileName string) *ThemeMetaInfo {
themeInfo := &ThemeMetaInfo{
FileName: fileName,
InternalName: strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix),
}
themeInfo.DisplayName = themeInfo.InternalName
return themeInfo
}

func defaultThemeMetaInfoByInternalName(fileName string) *ThemeMetaInfo {
return defaultThemeMetaInfoByFileName(fileNamePrefix + fileName + fileNameSuffix)
}

func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
themeInfo := defaultThemeMetaInfoByFileName(fileName)
themeInfo.PreferColorSchemes = parseThemePreferColorSchemes(cssContent)
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
m := parseThemeMetaInfoToMap(cssContent)
if m == nil {
return themeInfo
}
themeInfo.DisplayName = m["--theme-display-name"]
return themeInfo
}

func initThemes() {
availableThemes = nil
defer func() {
availableThemesSet = container.SetOf(availableThemes...)
if !availableThemesSet.Contains(setting.UI.DefaultTheme) {
availableThemeInternalNames = container.Set[string]{}
for _, theme := range availableThemes {
availableThemeInternalNames.Add(theme.InternalName)
}
if !availableThemeInternalNames.Contains(setting.UI.DefaultTheme) {
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)
}
}()
cssFiles, err := public.AssetFS().ListFiles("/assets/css")
if err != nil {
log.Error("Failed to list themes: %v", err)
availableThemes = []string{setting.UI.DefaultTheme}
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
return
}
var foundThemes []string
for _, name := range cssFiles {
name, ok := strings.CutPrefix(name, "theme-")
if !ok {
continue
}
name, ok = strings.CutSuffix(name, ".css")
if !ok {
continue
var foundThemes []*ThemeMetaInfo
for _, fileName := range cssFiles {
if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
content, err := public.AssetFS().ReadFile("/assets/css/" + fileName)
if err != nil {
log.Error("Failed to read theme file %q: %v", fileName, err)
continue
}
foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
}
foundThemes = append(foundThemes, name)
}
if len(setting.UI.Themes) > 0 {
allowedThemes := container.SetOf(setting.UI.Themes...)
for _, theme := range foundThemes {
if allowedThemes.Contains(theme) {
if allowedThemes.Contains(theme.InternalName) {
availableThemes = append(availableThemes, theme)
}
}
} else {
availableThemes = foundThemes
}
sort.Strings(availableThemes)
sort.Slice(availableThemes, func(i, j int) bool {
if availableThemes[i].InternalName == setting.UI.DefaultTheme {
return true
}
return availableThemes[i].DisplayName < availableThemes[j].DisplayName
})
if len(availableThemes) == 0 {
setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme")
availableThemes = []string{setting.UI.DefaultTheme}
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
}
}

func GetAvailableThemes() []string {
func GetAvailableThemes() []*ThemeMetaInfo {
themeOnce.Do(initThemes)
return availableThemes
}

func IsThemeAvailable(name string) bool {
func IsThemeAvailable(internalName string) bool {
themeOnce.Do(initThemes)
return availableThemesSet.Contains(name)
return availableThemeInternalNames.Contains(internalName)
}
20 changes: 20 additions & 0 deletions services/webtheme/webtheme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package webtheme

import (
"testing"

"code.gitea.io/gitea/modules/container"

"github.com/stretchr/testify/assert"
)

func TestParseThemeMetaInfo(t *testing.T) {
m := parseThemeMetaInfoToMap(`gitea-theme-meta-info { --k1: "v1"; --k2: "a\"b"; }`)
assert.Equal(t, map[string]string{"--k1": "v1", "--k2": `a"b`}, m)

schemes := parseThemePreferColorSchemes(`@media (prefers-color-scheme: dark) {} @media (prefers-color-scheme: light) {} @media (prefers-color-scheme:dark) {}`)
assert.Equal(t, container.SetOf("dark", "light"), schemes)
}
2 changes: 1 addition & 1 deletion templates/user/settings/appearance.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<label>{{ctx.Locale.Tr "settings.ui"}}</label>
<select name="theme" class="ui dropdown">
{{range $theme := .AllThemes}}
<option value="{{$theme}}" {{Iif (eq $.SignedUser.Theme $theme) "selected"}}>{{$theme}}</option>
<option value="{{$theme.InternalName}}" {{Iif (eq $.SignedUser.Theme $theme.InternalName) "selected"}}>{{$theme.DisplayName}}</option>
{{end}}
</select>
</div>
Expand Down
4 changes: 4 additions & 0 deletions web_src/css/themes/theme-gitea-auto.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
@import "./theme-gitea-light.css" (prefers-color-scheme: light);
@import "./theme-gitea-dark.css" (prefers-color-scheme: dark);

gitea-theme-meta-info {
--theme-display-name: "Auto";
}
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import "./theme-gitea-dark.css";

gitea-theme-meta-info {
--theme-display-name: "Dark (Red/Green Colorblind-Friendly)";
}

/* red/green colorblind-friendly colors */
/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */
:root {
Expand Down
4 changes: 4 additions & 0 deletions web_src/css/themes/theme-gitea-dark.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
@import "../chroma/dark.css";
@import "../codemirror/dark.css";

gitea-theme-meta-info {
--theme-display-name: "Dark";
}

:root {
--is-dark-theme: true;
--color-primary: #4183c4;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import "./theme-gitea-light.css";

gitea-theme-meta-info {
--theme-display-name: "Light (Red/Green Colorblind-Friendly)";
}

/* red/green colorblind-friendly colors */
/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */
:root {
Expand Down
4 changes: 4 additions & 0 deletions web_src/css/themes/theme-gitea-light.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
@import "../chroma/light.css";
@import "../codemirror/light.css";

gitea-theme-meta-info {
--theme-display-name: "Light";
}

:root {
--is-dark-theme: false;
--color-primary: #4183c4;
Expand Down