Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Contributor

Choose a reason for hiding this comment

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

You should not use context.Background(), as always.

And the AfterLoad could be completely removed, if you have read the code.

The UserThemeName used by templates is already able to handle empty theme name correctly

}
}

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion models/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func TestCreateUserInvalidEmail(t *testing.T) {
Email: "[email protected]\r\n",
Passwd: ";p['////..-++']",
IsAdmin: false,
Theme: setting.UI.DefaultTheme,
Theme: setting.Config().Theme.DefaultTheme.Value(t.Context()),
MustChangePassword: false,
}

Expand Down
3 changes: 2 additions & 1 deletion modules/fileicon/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package fileicon

import (
"context"
"html/template"
"strings"

Expand Down Expand Up @@ -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" {
Copy link
Contributor

Choose a reason for hiding this comment

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

No context.Background() please

I have spent enough time on cleaning up the rubbishes like

You have reviewed and approved those.

If you don't agree that "context.Background()" should be avoided, just tell me and BLOCK my PRs.

return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
}
return BasicEntryIconHTML(entry)
Expand Down
10 changes: 10 additions & 0 deletions modules/setting/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -53,6 +58,7 @@ type RepositoryStruct struct {
}

type ConfigStruct struct {
Theme *ThemeStruct
Picture *PictureStruct
Repository *RepositoryStruct
}
Expand All @@ -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"}),
Expand Down
4 changes: 0 additions & 4 deletions modules/setting/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:"},
Expand Down
2 changes: 2 additions & 0 deletions modules/structs/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package templates

import (
"context"
"fmt"
"html/template"
"net/url"
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions routers/api/v1/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why it needs to be exposed.

What's the real use case? Who need it? If it is important, why no test to make sure it is a stable API?

AllowedReactions: setting.UI.Reactions,
CustomEmojis: setting.UI.CustomEmojis,
})
}

Expand Down
5 changes: 5 additions & 0 deletions routers/web/admin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion services/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestCreateUser(t *testing.T) {
Email: "[email protected]",
Passwd: ";p['////..-++']",
IsAdmin: false,
Theme: setting.UI.DefaultTheme,
Theme: setting.Config().Theme.DefaultTheme.Value(t.Context()),
MustChangePassword: false,
}

Expand Down
12 changes: 7 additions & 5 deletions services/webtheme/webtheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package webtheme

import (
"context"
"regexp"
"sort"
"strings"
Expand Down Expand Up @@ -107,19 +108,20 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {

func initThemes() {
availableThemes = nil
defaultTheme := setting.Config().Theme.DefaultTheme.Value(context.Background())
Copy link
Contributor

Choose a reason for hiding this comment

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

initThemes is called in "once", but defaultTheme can be dynamically changed now, so how could the logic be right for a changed "default theme"?

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
Expand All @@ -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)}
}
}

Expand Down
2 changes: 2 additions & 0 deletions templates/admin/config_settings/config_settings.tmpl
Original file line number Diff line number Diff line change
@@ -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" .}}
Expand Down
36 changes: 36 additions & 0 deletions templates/admin/config_settings/theme.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.config.theme"}}
</h4>
<div class="ui attached table segment dropdown-container">
<dl class="admin-dl-horizontal">
<dt>{{ctx.Locale.Tr "admin.config.default_theme"}}</dt>
<dd>
<div class="ui selection dropdown js-theme-config-dropdown" data-config-key="theme.default_theme">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="text">
<strong>{{$.SystemConfig.Theme.DefaultTheme.Value ctx}}</strong>
</div>
<div class="menu">
{{range .AvailableThemes}}
<div class="{{if eq ($.SystemConfig.Theme.DefaultTheme.Value ctx) .DisplayName}}active selected {{end}}item" data-value="{{.DisplayName}}">{{.DisplayName}}</div>
{{end}}
</div>
</div>
</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.default_file_icon_theme"}}</dt>
<dd>
<div class="ui selection dropdown js-theme-config-dropdown" data-config-key="theme.default_file_icon_theme">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="text">
<strong>{{$.SystemConfig.Theme.DefaultFileIconTheme.Value ctx}}</strong>
</div>
<div class="menu">
{{range .AvailableFileIconThemes}}
<div class="{{if eq ($.SystemConfig.Theme.DefaultFileIconTheme.Value ctx) .}}active selected {{end}}item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
</dd>
</dl>
</div>
5 changes: 5 additions & 0 deletions templates/swagger/v1_json.tmpl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion tests/integration/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import (
"time"

repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
Expand Down Expand Up @@ -223,7 +225,13 @@ func testViewRepo1CloneLinkAuthorized(t *testing.T) {

func testViewRepoWithSymlinks(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.UI.FileIconTheme, "basic")()
defer func() {
err := system_model.SetSettings(t.Context(), map[string]string{
setting.Config().Theme.DefaultFileIconTheme.DynKey(): "basic",
})
assert.NoError(t, err)
config.GetDynGetter().InvalidateCache()
}()
Comment on lines +228 to +234
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure what you are doing?

defer test.MockVariableValue(&setting.UI.FileIconTheme, "basic")() means "use basic theme" for this test, and revert it after this test.

But your "defer" is only executed after the test?

session := loginUser(t, "user2")

req := NewRequest(t, "GET", "/user2/repo20.git")
Expand Down
6 changes: 6 additions & 0 deletions web_src/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
.admin dl.admin-dl-horizontal dd {
line-height: var(--line-height-default);
padding: 5px 0;
display: flex;
align-items: center;
}

.admin dl.admin-dl-horizontal dt {
Expand All @@ -39,6 +41,10 @@
overflow-x: auto; /* if the screen width is small, many wide tables (eg: user list) need scroll bars */
}

.admin .ui.table.segment.dropdown-container {
overflow: visible; /* allow dropdown menus to extend beyond container boundaries */
}
Copy link
Member

Choose a reason for hiding this comment

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

Which problematic overflow: hidden in the ancestors is causing this? Can we possibly remove it?


.admin .table th {
white-space: nowrap;
}
Expand Down
25 changes: 25 additions & 0 deletions web_src/js/features/admin/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {showTemporaryTooltip} from '../../modules/tippy.ts';
import {POST} from '../../modules/fetch.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';

const {appSubUrl} = window.config;

Expand All @@ -21,4 +22,28 @@ export function initAdminConfigs(): void {
}
});
}

// Handle theme config dropdowns
for (const el of elAdminConfig.querySelectorAll('.js-theme-config-dropdown')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In the future, every time you introduce a "system setting config option", then you will copy&paste another tens of lines code?

fomanticQuery(el).dropdown({
async onChange(value: string, _text: string, _$item: any) {
if (!value) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why !value would happen?


const configKey = this.getAttribute('data-config-key');
if (!configKey) return;
Comment on lines +32 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

Why !configKey would happen?


try {
const resp = await POST(`${appSubUrl}/-/admin/config`, {
data: new URLSearchParams({key: configKey, value}),
});
const json: Record<string, any> = await resp.json();
if (json.errorMessage) throw new Error(json.errorMessage);
} catch (ex) {
showTemporaryTooltip(this, ex.toString());
// Revert the dropdown to the previous value on error
fomanticQuery(el).dropdown('restore defaults');
}
},
});
}
}
Loading