diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b4e330184e..12588c1387 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1231,7 +1231,8 @@ LEVEL = Info ;DEFAULT_THEME = gitea-auto ;; ;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`. -;THEMES = gitea-auto,gitea-light,gitea-dark +;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" +;THEMES = ;; ;; All available reactions users can choose on issues/prs and comments. ;; Values can be emoji alias (:smile:) or a unicode emoji. diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 9328177f50..b295ddf53a 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -214,10 +214,9 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SITEMAP_PAGING_NUM`: **20**: Number of items that are displayed in a single subsitemap. - `GRAPH_MAX_COMMIT_NUM`: **100**: Number of maximum commits shown in the commit graph. - `CODE_COMMENT_LINES`: **4**: Number of line of codes shown for a code comment. -- `DEFAULT_THEME`: **gitea-auto**: \[gitea-auto, gitea-light, gitea-dark\]: Set the default theme for the Gitea installation. +- `DEFAULT_THEME`: **gitea-auto**: Set the default theme for the Gitea installation, custom themes could be provided by "{CustomPath}/public/assets/css/theme-*.css". - `SHOW_USER_EMAIL`: **true**: Whether the email of the user should be shown in the Explore Users page. -- `THEMES`: **gitea-auto,gitea-light,gitea-dark**: All available themes. Allow users select personalized themes. - regardless of the value of `DEFAULT_THEME`. +- `THEMES`: **_empty_**: All available themes by "{CustomPath}/public/assets/css/theme-*.css". Allow users select personalized themes. - `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB) - `AMBIGUOUS_UNICODE_DETECTION`: **true**: Detect ambiguous unicode characters in file contents and show warnings on the UI - `REACTIONS`: All available reactions users can choose on issues/prs and comments diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index e4945dd1c1..0d08a5e51b 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -212,10 +212,9 @@ menu: - `SITEMAP_PAGING_NUM`: **20**: 在单个子SiteMap中显示的项数。 - `GRAPH_MAX_COMMIT_NUM`: **100**: 提交图中显示的最大commit数量。 - `CODE_COMMENT_LINES`: **4**: 在代码评论中能够显示的最大代码行数。 -- `DEFAULT_THEME`: **gitea-auto**: \[gitea-auto, gitea-light, gitea-dark\]: 在Gitea安装时候设置的默认主题。 +- `DEFAULT_THEME`: **gitea-auto**: 在Gitea安装时候设置的默认主题,自定义的主题可以通过 "{CustomPath}/public/assets/css/theme-*.css" 提供。 - `SHOW_USER_EMAIL`: **true**: 用户的电子邮件是否应该显示在`Explore Users`页面中。 -- `THEMES`: **gitea-auto,gitea-light,gitea-dark**: 所有可用的主题。允许用户选择个性化的主题, - 而不受DEFAULT_THEME 值的影响。 +- `THEMES`: **_empty_**: 所有可用的主题(由 "{CustomPath}/public/assets/css/theme-*.css" 提供)。允许用户选择个性化的主题, - `MAX_DISPLAY_FILE_SIZE`: **8388608**: 能够显示文件的最大大小(默认为8MiB)。 - `REACTIONS`: 用户可以在问题(Issue)、Pull Request(PR)以及评论中选择的所有可选的反应。 这些值可以是表情符号别名(例如::smile:)或Unicode表情符号。 diff --git a/docs/content/administration/customizing-gitea.en-us.md b/docs/content/administration/customizing-gitea.en-us.md index 7efddb2824..8475f6d131 100644 --- a/docs/content/administration/customizing-gitea.en-us.md +++ b/docs/content/administration/customizing-gitea.en-us.md @@ -381,7 +381,7 @@ To make a custom theme available to all users: 1. Add a CSS file to `$GITEA_CUSTOM/public/assets/css/theme-.css`. The value of `$GITEA_CUSTOM` of your instance can be queried by calling `gitea help` and looking up the value of "CustomPath". -2. Add `` to the comma-separated list of setting `THEMES` in `app.ini` +2. Add `` to the comma-separated list of setting `THEMES` in `app.ini`, or leave `THEMES` empty to allow all themes. Community themes are listed in [gitea/awesome-gitea#themes](https://gitea.com/gitea/awesome-gitea#themes). diff --git a/docs/content/help/faq.en-us.md b/docs/content/help/faq.en-us.md index b3b0980125..ba39ec83b0 100644 --- a/docs/content/help/faq.en-us.md +++ b/docs/content/help/faq.en-us.md @@ -178,17 +178,6 @@ At some point, a customer or third party needs access to a specific repo and onl Use [Fail2Ban](administration/fail2ban-setup.md) to monitor and stop automated login attempts or other malicious behavior based on log patterns -## How to add/use custom themes - -Gitea supports three official themes right now, `gitea-light`, `gitea-dark`, and `gitea-auto` (automatically switches between the previous two depending on operating system settings). -To add your own theme, currently the only way is to provide a complete theme (not just color overrides) - -As an example, let's say our theme is `arc-blue` (this is a real theme, and can be found [in this issue](https://github.com/go-gitea/gitea/issues/6011)) - -Name the `.css` file `theme-arc-blue.css` and add it to your custom folder in `custom/public/assets/css` - -Allow users to use it by adding `arc-blue` to the list of `THEMES` in your `app.ini` - ## SSHD vs built-in SSH SSHD is the built-in SSH server on most Unix systems. diff --git a/docs/content/help/faq.zh-cn.md b/docs/content/help/faq.zh-cn.md index 25230df70b..ef8a149ae2 100644 --- a/docs/content/help/faq.zh-cn.md +++ b/docs/content/help/faq.zh-cn.md @@ -182,17 +182,6 @@ Gitea不提供内置的Pages服务器。您需要一个专用的域名来提供 使用 [Fail2Ban](administration/fail2ban-setup.md) 监视并阻止基于日志模式的自动登录尝试或其他恶意行为。 -## 如何添加/使用自定义主题 - -Gitea 目前支持三个官方主题,分别是 `gitea-light`、`gitea-dark` 和 `gitea-auto`(根据操作系统设置自动切换前两个主题)。 -要添加自己的主题,目前唯一的方法是提供一个完整的主题(不仅仅是颜色覆盖)。 - -假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到) - -将`.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/assets/css`文件夹中 - -通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题 - ## SSHD vs 内建SSH SSHD是大多数Unix系统上内建的SSH服务器。 diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 03f27ba203..3138f8a63e 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -318,7 +318,7 @@ func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { // StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc var StartupProblems []string -func logStartupProblem(skip int, level log.Level, format string, args ...any) { +func LogStartupProblem(skip int, level log.Level, format string, args ...any) { msg := fmt.Sprintf(format, args...) log.Log(skip+1, level, "%s", msg) StartupProblems = append(StartupProblems, msg) @@ -326,14 +326,14 @@ func logStartupProblem(skip int, level log.Level, format string, args ...any) { func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) { if rootCfg.Section(oldSection).HasKey(oldKey) { - logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) + LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) } } // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { if rootCfg.Section(oldSection).HasKey(oldKey) { - logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) + LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) } } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 6930197b22..34e1a336dc 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -174,7 +174,7 @@ func GetGeneralTokenSigningSecret() []byte { } if generalSigningSecret.CompareAndSwap(old, &jwtSecret) { // FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...) - logStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes") + LogStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes") return jwtSecret } return *generalSigningSecret.Load() diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 92bb0b6541..f056fbfc6c 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -235,7 +235,7 @@ var configuredPaths = make(map[string]string) func checkOverlappedPath(name, path string) { // TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path) if targetName, ok := configuredPaths[path]; ok && targetName != name { - logStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name) + LogStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name) } configuredPaths[path] = name } diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 2f9eef93c3..93855bca07 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -82,7 +82,6 @@ var UI = struct { ReactionMaxUserNum: 10, MaxDisplayFileSize: 8388608, DefaultTheme: `gitea-auto`, - Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`}, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 360b48c594..94464fe628 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/gitdiff" + "code.gitea.io/gitea/services/webtheme" ) // NewFuncMap returns functions for injecting to templates @@ -137,12 +138,7 @@ func NewFuncMap() template.FuncMap { "DisableImportLocal": func() bool { return !setting.ImportLocalPaths }, - "ThemeName": func(user *user_model.User) string { - if user == nil || user.Theme == "" { - return setting.UI.DefaultTheme - } - return user.Theme - }, + "UserThemeName": UserThemeName, "NotificationSettings": func() map[string]any { return map[string]any{ "MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond), @@ -261,3 +257,13 @@ func Eval(tokens ...any) (any, error) { n, err := eval.Expr(tokens...) return n.Value, err } + +func UserThemeName(user *user_model.User) string { + if user == nil || user.Theme == "" { + return setting.UI.DefaultTheme + } + if webtheme.IsThemeAvailable(user.Theme) { + return user.Theme + } + return setting.UI.DefaultTheme +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c7d99a85b1..4f17b1a6db 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -763,6 +763,8 @@ manage_themes = Select default theme manage_openid = Manage 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. theme_desc = This will be your default theme across the site. +theme_colorblindness_help = Colorblindness Theme Support +theme_colorblindness_prompt = Gitea just gets some themes with basic colorblindness support, which only have a few colors defined. The work is still in progress. More improvements could be done by defining more colors in the theme CSS files. primary = Primary activated = Activated requires_activation = Requires activation diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 49eb050dcb..e5ff8570cf 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" user_service "code.gitea.io/gitea/services/user" + "code.gitea.io/gitea/services/webtheme" ) const ( @@ -319,6 +320,13 @@ 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 + var hiddenCommentTypes *big.Int val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) if err != nil { @@ -341,11 +349,12 @@ func UpdateUIThemePost(ctx *context.Context) { ctx.Data["PageIsSettingsAppearance"] = true if ctx.HasError() { + ctx.Flash.Error(ctx.GetErrMsg()) ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") return } - if !form.IsThemeExists() { + if !webtheme.IsThemeAvailable(form.Theme) { ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") return diff --git a/routers/web/web.go b/routers/web/web.go index 994e639e20..c6132f0d61 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -652,7 +652,7 @@ func registerRoutes(m *web.Route) { m.Get("", user_setting.BlockedUsers) m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost) }) - }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled)) + }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled)) m.Group("/user", func() { m.Get("/activate", auth.Activate) diff --git a/services/context/context.go b/services/context/context.go index 1641e995fb..88ab5cae0e 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -230,6 +230,7 @@ func Contexter() func(next http.Handler) http.Handler { // HasError returns true if error occurs in form validation. // Attention: this function changes ctx.Data and ctx.Flash +// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again. func (ctx *Context) HasError() bool { hasErr, ok := ctx.Data["HasError"] if !ok { diff --git a/services/forms/user_form.go b/services/forms/user_form.go index e2e6c208f7..418a87b863 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -11,7 +11,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/context" @@ -273,7 +272,7 @@ func (f *AddEmailForm) Validate(req *http.Request, errs binding.Errors) binding. // UpdateThemeForm form for updating a users' theme type UpdateThemeForm struct { - Theme string `binding:"Required;MaxSize(30)"` + Theme string `binding:"Required;MaxSize(255)"` } // Validate validates the field @@ -282,20 +281,6 @@ func (f *UpdateThemeForm) Validate(req *http.Request, errs binding.Errors) bindi return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// IsThemeExists checks if the theme is a theme available in the config. -func (f UpdateThemeForm) IsThemeExists() bool { - var exists bool - - for _, v := range setting.UI.Themes { - if strings.EqualFold(v, f.Theme) { - exists = true - break - } - } - - return exists -} - // ChangePasswordForm form for changing password type ChangePasswordForm struct { OldPassword string `form:"old_password" binding:"MaxSize(255)"` diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go new file mode 100644 index 0000000000..dc801e1ff7 --- /dev/null +++ b/services/webtheme/webtheme.go @@ -0,0 +1,74 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package webtheme + +import ( + "sort" + "strings" + "sync" + + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/public" + "code.gitea.io/gitea/modules/setting" +) + +var ( + availableThemes []string + availableThemesSet container.Set[string] + themeOnce sync.Once +) + +func initThemes() { + availableThemes = nil + defer func() { + availableThemesSet = container.SetOf(availableThemes...) + if !availableThemesSet.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} + 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 + } + foundThemes = append(foundThemes, name) + } + if len(setting.UI.Themes) > 0 { + allowedThemes := container.SetOf(setting.UI.Themes...) + for _, theme := range foundThemes { + if allowedThemes.Contains(theme) { + availableThemes = append(availableThemes, theme) + } + } + } else { + availableThemes = foundThemes + } + sort.Strings(availableThemes) + 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} + } +} + +func GetAvailableThemes() []string { + themeOnce.Do(initThemes) + return availableThemes +} + +func IsThemeAvailable(name string) bool { + themeOnce.Do(initThemes) + return availableThemesSet.Contains(name) +} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 2de8f58235..174267fd2f 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -1,5 +1,5 @@ - + {{if .Title}}{{.Title}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}} diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 0793eaca20..f97e1880ce 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - + diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index 576b6eebbb..566fddcec1 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,12 +1,12 @@ {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics. -* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName +* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, UserThemeName * ctx.Locale * .Flash * .ErrorMsg * .SignedUser (optional) */}} - + Internal Server Error - {{AppName}} diff --git a/templates/user/settings/appearance.tmpl b/templates/user/settings/appearance.tmpl index 0997d721e1..4fa248910a 100644 --- a/templates/user/settings/appearance.tmpl +++ b/templates/user/settings/appearance.tmpl @@ -1,4 +1,4 @@ -{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings sshkeys")}} +{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings")}}
@@ -6,39 +6,26 @@ {{ctx.Locale.Tr "settings.manage_themes"}}
-
diff --git a/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css b/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css new file mode 100644 index 0000000000..681aa3b539 --- /dev/null +++ b/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css @@ -0,0 +1,11 @@ +@import "./theme-gitea-dark.css"; + +/* red/green colorblind-friendly colors */ +/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */ +:root { + --color-diff-added-word-bg: #388bfd66; + --color-diff-added-row-bg: #388bfd26; + + --color-diff-removed-word-bg: #db6d2866; + --color-diff-removed-row-bg: #db6d2826; +} diff --git a/web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css b/web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css new file mode 100644 index 0000000000..7e03d90f5c --- /dev/null +++ b/web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css @@ -0,0 +1,11 @@ +@import "./theme-gitea-light.css"; + +/* red/green colorblind-friendly colors */ +/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */ +:root { + --color-diff-added-word-bg: #54aeff66; + --color-diff-added-row-bg: #ddf4ff80; + + --color-diff-removed-word-bg: #ffb77c80; + --color-diff-removed-row-bg: #fff1e580; +}