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 4 commits
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
20 changes: 17 additions & 3 deletions docs/content/administration/customizing-gitea.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,13 +383,27 @@ To make a custom theme available to all users:
The value of `$GITEA_CUSTOM` of your instance can be queried by calling `gitea help` and looking up the value of "CustomPath".
2. Add `<theme-name>` to the comma-separated list of setting `THEMES` in `app.ini`, or leave `THEMES` empty to allow all themes.

A custom theme file named `theme-my-theme.css` will be displayed as `my-theme` on the user's theme selection page.
It could add theme meta information into the custom theme CSS file to provide more information about the theme.

If a custom theme is a dark theme, please set the global css variable `--is-dark-theme: true` in the `:root` block.
This allows Gitea to adjust the Monaco code editor's theme accordingly.

```css
gitea-theme-meta-info {
--theme-display-name: "My Awesome Theme"; /* this theme will be display as "My Awesome Theme" on the UI */
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
}
:root {
--is-dark-theme: true; /* if it is a dark theme */
--color-primary: #112233;
/* more custom theme variables ... */
}
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs example how to implement a "auto" theme.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think it needs an example, just copy the "gitea-auto.css"

We can't teach everything here, just like we don't need to explain the --color-xxx one by one.

If you have ideas about how to implement an "auto" theme, feel free to edit the documents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the document fdfd440

Community themes are listed in [gitea/awesome-gitea#themes](https://gitea.com/gitea/awesome-gitea#themes).

The default theme sources can be found [here](https://github.com/go-gitea/gitea/blob/main/web_src/css/themes).

If your custom theme is considered a dark theme, set the global css variable `--is-dark-theme` to `true`.
This allows Gitea to adjust the Monaco code editor's theme accordingly.

## Customizing fonts

Fonts can be customized using CSS variables:
Expand Down
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
128 changes: 106 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,146 @@ 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
}

func parseThemeMetaInfoToMap(cssContent string) map[string]string {
/*
The theme meta info is stored in the CSS file's variables of `gitea-theme-meta-info` element,
which is a privately defined and is only used by backend to extract the meta info.
Not using ":root" because it is difficult to parse various ":root" blocks when importing other files,
it is difficult to control the overriding, and it's difficult to avoid user's customized overridden styles.
*/
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

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)
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)
}
15 changes: 15 additions & 0 deletions services/webtheme/webtheme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package webtheme

import (
"testing"

"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)
}
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