diff --git a/models/user/user.go b/models/user/user.go index 6143992a2537b..fcfd8a74a18d1 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -201,7 +201,7 @@ func (u *User) BeforeUpdate() { // AfterLoad is invoked from XORM after filling all the fields of this object. func (u *User) AfterLoad() { if u.Theme == "" { - u.Theme = setting.UI.DefaultTheme + u.Theme = setting.Config().Theme.DefaultTheme.Value(context.Background()) } } @@ -663,7 +663,7 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification u.MaxRepoCreation = -1 - u.Theme = setting.UI.DefaultTheme + u.Theme = setting.Config().Theme.DefaultTheme.Value(ctx) u.IsRestricted = setting.Service.DefaultUserIsRestricted u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm) diff --git a/models/user/user_test.go b/models/user/user_test.go index 4201ec4816c86..2ae3565832c7c 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -276,7 +276,7 @@ func TestCreateUserInvalidEmail(t *testing.T) { Email: "GiteaBot@gitea.io\r\n", Passwd: ";p['////..-++']", IsAdmin: false, - Theme: setting.UI.DefaultTheme, + Theme: setting.Config().Theme.DefaultTheme.Value(t.Context()), MustChangePassword: false, } diff --git a/modules/fileicon/render.go b/modules/fileicon/render.go index 8ed86b9ac0eb9..cb5d7627edaf5 100644 --- a/modules/fileicon/render.go +++ b/modules/fileicon/render.go @@ -4,6 +4,7 @@ package fileicon import ( + "context" "html/template" "strings" @@ -34,7 +35,7 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML { } func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML { - if setting.UI.FileIconTheme == "material" { + if setting.Config().Theme.DefaultFileIconTheme.Value(context.Background()) == "material" { return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry) } return BasicEntryIconHTML(entry) diff --git a/modules/setting/config.go b/modules/setting/config.go index 4c5d2df7d8a01..42d9702e3bcf0 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -10,6 +10,11 @@ import ( "code.gitea.io/gitea/modules/setting/config" ) +type ThemeStruct struct { + DefaultTheme *config.Value[string] + DefaultFileIconTheme *config.Value[string] +} + type PictureStruct struct { DisableGravatar *config.Value[bool] EnableFederatedAvatar *config.Value[bool] @@ -53,6 +58,7 @@ type RepositoryStruct struct { } type ConfigStruct struct { + Theme *ThemeStruct Picture *PictureStruct Repository *RepositoryStruct } @@ -65,6 +71,10 @@ var ( func initDefaultConfig() { config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) defaultConfig = &ConfigStruct{ + Theme: &ThemeStruct{ + DefaultTheme: config.ValueJSON[string]("theme.default_theme").WithFileConfig(config.CfgSecKey{Sec: "ui", Key: "DEFAULT_THEME"}).WithDefault("gitea-auto"), + DefaultFileIconTheme: config.ValueJSON[string]("theme.default_file_icon_theme").WithFileConfig(config.CfgSecKey{Sec: "ui", Key: "FILE_ICON_THEME"}).WithDefault("material"), + }, Picture: &PictureStruct{ DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 3d9c916bf7f72..330edc2691789 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -26,9 +26,7 @@ var UI = struct { MaxDisplayFileSize int64 ShowUserEmail bool DefaultShowFullName bool - DefaultTheme string Themes []string - FileIconTheme string Reactions []string ReactionsLookup container.Set[string] `ini:"-"` CustomEmojis []string @@ -84,8 +82,6 @@ var UI = struct { CodeCommentLines: 4, ReactionMaxUserNum: 10, MaxDisplayFileSize: 8388608, - DefaultTheme: `gitea-auto`, - FileIconTheme: `material`, 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/structs/settings.go b/modules/structs/settings.go index 403afda9ff5bc..02a5e83b69d8e 100644 --- a/modules/structs/settings.go +++ b/modules/structs/settings.go @@ -23,6 +23,8 @@ type GeneralRepoSettings struct { type GeneralUISettings struct { // DefaultTheme is the default UI theme DefaultTheme string `json:"default_theme"` + // DefaultFileIconTheme is the default file icon theme + DefaultFileIconTheme string `json:"default_file_icon_theme"` // AllowedReactions contains the list of allowed emoji reactions AllowedReactions []string `json:"allowed_reactions"` // CustomEmojis contains the list of custom emojis diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e454bce4bd3c2..7d66cb4e45ec6 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -5,6 +5,7 @@ package templates import ( + "context" "fmt" "html/template" "net/url" @@ -218,13 +219,14 @@ func evalTokens(tokens ...any) (any, error) { } func userThemeName(user *user_model.User) string { + defaultTheme := setting.Config().Theme.DefaultTheme.Value(context.Background()) if user == nil || user.Theme == "" { - return setting.UI.DefaultTheme + return defaultTheme } if webtheme.IsThemeAvailable(user.Theme) { return user.Theme } - return setting.UI.DefaultTheme + return defaultTheme } func isQueryParamEmpty(v any) bool { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b3eb7b1f4a4fd..78d2ce5e9c0dd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3428,6 +3428,9 @@ config.session_life_time = Session Life Time config.https_only = HTTPS Only config.cookie_life_time = Cookie Life Time +config.theme = Theme Configuration +config.default_theme = Default Theme +config.default_file_icon_theme = Default File Icon Theme config.picture_config = Picture and Avatar Configuration config.picture_service = Picture Service config.disable_gravatar = Disable Gravatar diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go index 94fbadeab0188..23d24b0cb9c19 100644 --- a/routers/api/v1/settings/settings.go +++ b/routers/api/v1/settings/settings.go @@ -22,9 +22,10 @@ func GetGeneralUISettings(ctx *context.APIContext) { // "200": // "$ref": "#/responses/GeneralUISettings" ctx.JSON(http.StatusOK, api.GeneralUISettings{ - DefaultTheme: setting.UI.DefaultTheme, - AllowedReactions: setting.UI.Reactions, - CustomEmojis: setting.UI.CustomEmojis, + DefaultTheme: setting.Config().Theme.DefaultTheme.Value(ctx), + DefaultFileIconTheme: setting.Config().Theme.DefaultFileIconTheme.Value(ctx), + AllowedReactions: setting.UI.Reactions, + CustomEmojis: setting.UI.CustomEmojis, }) } diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 774b31ab9842a..421ff9cf989b4 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/mailer" + "code.gitea.io/gitea/services/webtheme" "gitea.com/go-chi/session" ) @@ -192,6 +193,8 @@ func ConfigSettings(ctx *context.Context) { ctx.Data["PageIsAdminConfig"] = true ctx.Data["PageIsAdminConfigSettings"] = true ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString() + ctx.Data["AvailableThemes"] = webtheme.GetAvailableThemes() + ctx.Data["AvailableFileIconThemes"] = []string{"material", "basic"} ctx.HTML(http.StatusOK, tplConfigSettings) } @@ -231,6 +234,8 @@ func ChangeConfig(ctx *context.Context) { return json.Marshal(openWithEditorApps) } marshallers := map[string]func(string) ([]byte, error){ + cfg.Theme.DefaultTheme.DynKey(): marshalString(cfg.Theme.DefaultTheme.DefaultValue()), + cfg.Theme.DefaultFileIconTheme.DynKey(): marshalString(cfg.Theme.DefaultFileIconTheme.DefaultValue()), cfg.Picture.DisableGravatar.DynKey(): marshalBool, cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool, cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps, diff --git a/services/user/user_test.go b/services/user/user_test.go index 48852b4cb9a39..64543d6c4e3f9 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -88,7 +88,7 @@ func TestCreateUser(t *testing.T) { Email: "GiteaBot@gitea.io", Passwd: ";p['////..-++']", IsAdmin: false, - Theme: setting.UI.DefaultTheme, + Theme: setting.Config().Theme.DefaultTheme.Value(t.Context()), MustChangePassword: false, } diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index 4e89d6dbac13a..54ad7e260cc48 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -4,6 +4,7 @@ package webtheme import ( + "context" "regexp" "sort" "strings" @@ -107,19 +108,20 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo { func initThemes() { availableThemes = nil + defaultTheme := setting.Config().Theme.DefaultTheme.Value(context.Background()) defer func() { 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) + if !availableThemeInternalNames.Contains(defaultTheme) { + setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", defaultTheme) } }() cssFiles, err := public.AssetFS().ListFiles("/assets/css") if err != nil { log.Error("Failed to list themes: %v", err) - availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)} + availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(defaultTheme)} return } var foundThemes []*ThemeMetaInfo @@ -144,14 +146,14 @@ func initThemes() { availableThemes = foundThemes } sort.Slice(availableThemes, func(i, j int) bool { - if availableThemes[i].InternalName == setting.UI.DefaultTheme { + if availableThemes[i].InternalName == 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 = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)} + availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(defaultTheme)} } } diff --git a/templates/admin/config_settings/config_settings.tmpl b/templates/admin/config_settings/config_settings.tmpl index 1ef764a58bac5..d8cfc96d26971 100644 --- a/templates/admin/config_settings/config_settings.tmpl +++ b/templates/admin/config_settings/config_settings.tmpl @@ -1,5 +1,7 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}} +{{template "admin/config_settings/theme" .}} + {{template "admin/config_settings/avatars" .}} {{template "admin/config_settings/repository" .}} diff --git a/templates/admin/config_settings/theme.tmpl b/templates/admin/config_settings/theme.tmpl new file mode 100644 index 0000000000000..7ed2bb9ed5501 --- /dev/null +++ b/templates/admin/config_settings/theme.tmpl @@ -0,0 +1,36 @@ +