* add migration and basic UI for changing a user's theme * update user themem * use right text on button * load theme based on users' selection * load theme based on users' selection in pwa too * update sample config * delete older theme loading * implement AfterLoad to set users' theme properly * set up default theme when creating a user. This uses the installation wide theme * use flash messages for error * set default theme when creating a user from the cli * fix @lunny reviewtags/v1.21.12.1
| @@ -340,6 +340,7 @@ func runCreateUser(c *cli.Context) error { | |||||
| IsActive: true, | IsActive: true, | ||||
| IsAdmin: c.Bool("admin"), | IsAdmin: c.Bool("admin"), | ||||
| MustChangePassword: changePassword, | MustChangePassword: changePassword, | ||||
| Theme: setting.UI.DefaultTheme, | |||||
| }); err != nil { | }); err != nil { | ||||
| return fmt.Errorf("CreateUser: %v", err) | return fmt.Errorf("CreateUser: %v", err) | ||||
| } | } | ||||
| @@ -85,6 +85,8 @@ MAX_DISPLAY_FILE_SIZE = 8388608 | |||||
| SHOW_USER_EMAIL = true | SHOW_USER_EMAIL = true | ||||
| ; Set the default theme for the Gitea install | ; Set the default theme for the Gitea install | ||||
| DEFAULT_THEME = gitea | DEFAULT_THEME = gitea | ||||
| ; All available themes | |||||
| THEMES = gitea,arc-green | |||||
| [ui.admin] | [ui.admin] | ||||
| ; Number of users that are displayed on one page | ; Number of users that are displayed on one page | ||||
| @@ -18,7 +18,7 @@ import ( | |||||
| "github.com/Unknwon/com" | "github.com/Unknwon/com" | ||||
| "github.com/go-xorm/xorm" | "github.com/go-xorm/xorm" | ||||
| gouuid "github.com/satori/go.uuid" | gouuid "github.com/satori/go.uuid" | ||||
| "gopkg.in/ini.v1" | |||||
| ini "gopkg.in/ini.v1" | |||||
| "code.gitea.io/gitea/modules/generate" | "code.gitea.io/gitea/modules/generate" | ||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| @@ -206,6 +206,8 @@ var migrations = []Migration{ | |||||
| NewMigration("clear nonused data which not deleted when user was deleted", clearNonusedData), | NewMigration("clear nonused data which not deleted when user was deleted", clearNonusedData), | ||||
| // v76 -> v77 | // v76 -> v77 | ||||
| NewMigration("add pull request rebase with merge commit", addPullRequestRebaseWithMerge), | NewMigration("add pull request rebase with merge commit", addPullRequestRebaseWithMerge), | ||||
| // v77 -> v78 | |||||
| NewMigration("add theme to users", addUserDefaultTheme), | |||||
| } | } | ||||
| // Migrate database to current version | // Migrate database to current version | ||||
| @@ -0,0 +1,17 @@ | |||||
| // Copyright 2019 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package migrations | |||||
| import ( | |||||
| "github.com/go-xorm/xorm" | |||||
| ) | |||||
| func addUserDefaultTheme(x *xorm.Engine) error { | |||||
| type User struct { | |||||
| Theme string `xorm:"VARCHAR(30)"` | |||||
| } | |||||
| return x.Sync2(new(User)) | |||||
| } | |||||
| @@ -140,6 +140,7 @@ type User struct { | |||||
| // Preferences | // Preferences | ||||
| DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` | DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` | ||||
| Theme string `xorm:"NOT NULL DEFAULT ''"` | |||||
| } | } | ||||
| // BeforeUpdate is invoked from XORM before updating this object. | // BeforeUpdate is invoked from XORM before updating this object. | ||||
| @@ -165,6 +166,13 @@ func (u *User) BeforeUpdate() { | |||||
| u.Description = base.TruncateString(u.Description, 255) | u.Description = base.TruncateString(u.Description, 255) | ||||
| } | } | ||||
| // 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 | |||||
| } | |||||
| } | |||||
| // SetLastLogin set time to last login | // SetLastLogin set time to last login | ||||
| func (u *User) SetLastLogin() { | func (u *User) SetLastLogin() { | ||||
| u.LastLoginUnix = util.TimeStampNow() | u.LastLoginUnix = util.TimeStampNow() | ||||
| @@ -176,6 +184,12 @@ func (u *User) UpdateDiffViewStyle(style string) error { | |||||
| return UpdateUserCols(u, "diff_view_style") | return UpdateUserCols(u, "diff_view_style") | ||||
| } | } | ||||
| // UpdateTheme updates a users' theme irrespective of the site wide theme | |||||
| func (u *User) UpdateTheme(themeName string) error { | |||||
| u.Theme = themeName | |||||
| return UpdateUserCols(u, "theme") | |||||
| } | |||||
| // getEmail returns an noreply email, if the user has set to keep his | // getEmail returns an noreply email, if the user has set to keep his | ||||
| // email address private, otherwise the primary email address. | // email address private, otherwise the primary email address. | ||||
| func (u *User) getEmail() string { | func (u *User) getEmail() string { | ||||
| @@ -777,6 +791,7 @@ func CreateUser(u *User) (err error) { | |||||
| u.HashPassword(u.Passwd) | u.HashPassword(u.Passwd) | ||||
| u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization | u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization | ||||
| u.MaxRepoCreation = -1 | u.MaxRepoCreation = -1 | ||||
| u.Theme = setting.UI.DefaultTheme | |||||
| if _, err = sess.Insert(u); err != nil { | if _, err = sess.Insert(u); err != nil { | ||||
| return err | return err | ||||
| @@ -12,7 +12,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
| "github.com/go-macaron/binding" | "github.com/go-macaron/binding" | ||||
| "gopkg.in/macaron.v1" | |||||
| macaron "gopkg.in/macaron.v1" | |||||
| ) | ) | ||||
| // InstallForm form for installation page | // InstallForm form for installation page | ||||
| @@ -189,6 +189,30 @@ func (f *AddEmailForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi | |||||
| return validate(errs, ctx.Data, f, ctx.Locale) | return validate(errs, ctx.Data, f, ctx.Locale) | ||||
| } | } | ||||
| // UpdateThemeForm form for updating a users' theme | |||||
| type UpdateThemeForm struct { | |||||
| Theme string `binding:"Required;MaxSize(30)"` | |||||
| } | |||||
| // Validate validates the field | |||||
| func (f *UpdateThemeForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||||
| return 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.ToLower(v) == strings.ToLower(f.Theme) { | |||||
| exists = true | |||||
| break | |||||
| } | |||||
| } | |||||
| return exists | |||||
| } | |||||
| // ChangePasswordForm form for changing password | // ChangePasswordForm form for changing password | ||||
| type ChangePasswordForm struct { | type ChangePasswordForm struct { | ||||
| OldPassword string `form:"old_password" binding:"MaxSize(255)"` | OldPassword string `form:"old_password" binding:"MaxSize(255)"` | ||||
| @@ -8,4 +8,5 @@ var ( | |||||
| defaultLangs = strings.Split("en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR", ",") | defaultLangs = strings.Split("en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR", ",") | ||||
| defaultLangNames = strings.Split("English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어", ",") | defaultLangNames = strings.Split("English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어", ",") | ||||
| defaultPullRequestWorkInProgressPrefixes = strings.Split("WIP:,[WIP]", ",") | defaultPullRequestWorkInProgressPrefixes = strings.Split("WIP:,[WIP]", ",") | ||||
| defaultThemes = strings.Split("gitea", "arc-green") | |||||
| ) | ) | ||||
| @@ -33,9 +33,9 @@ import ( | |||||
| "github.com/go-macaron/session" | "github.com/go-macaron/session" | ||||
| _ "github.com/go-macaron/session/redis" // redis plugin for store session | _ "github.com/go-macaron/session/redis" // redis plugin for store session | ||||
| "github.com/go-xorm/core" | "github.com/go-xorm/core" | ||||
| "github.com/kballard/go-shellquote" | |||||
| "github.com/mcuadros/go-version" | |||||
| "gopkg.in/ini.v1" | |||||
| shellquote "github.com/kballard/go-shellquote" | |||||
| version "github.com/mcuadros/go-version" | |||||
| ini "gopkg.in/ini.v1" | |||||
| "strk.kbt.io/projects/go/libravatar" | "strk.kbt.io/projects/go/libravatar" | ||||
| ) | ) | ||||
| @@ -303,6 +303,7 @@ var ( | |||||
| MaxDisplayFileSize int64 | MaxDisplayFileSize int64 | ||||
| ShowUserEmail bool | ShowUserEmail bool | ||||
| DefaultTheme string | DefaultTheme string | ||||
| Themes []string | |||||
| Admin struct { | Admin struct { | ||||
| UserPagingNum int | UserPagingNum int | ||||
| @@ -329,6 +330,7 @@ var ( | |||||
| ThemeColorMetaTag: `#6cc644`, | ThemeColorMetaTag: `#6cc644`, | ||||
| MaxDisplayFileSize: 8388608, | MaxDisplayFileSize: 8388608, | ||||
| DefaultTheme: `gitea`, | DefaultTheme: `gitea`, | ||||
| Themes: []string{`gitea`, `arc-green`}, | |||||
| Admin: struct { | Admin: struct { | ||||
| UserPagingNum int | UserPagingNum int | ||||
| RepoPagingNum int | RepoPagingNum int | ||||
| @@ -355,6 +355,7 @@ password_username_disabled = Non-local users are not allowed to change their use | |||||
| full_name = Full Name | full_name = Full Name | ||||
| website = Website | website = Website | ||||
| location = Location | location = Location | ||||
| update_theme = Update Theme | |||||
| update_profile = Update Profile | update_profile = Update Profile | ||||
| update_profile_success = Your profile has been updated. | update_profile_success = Your profile has been updated. | ||||
| change_username = Your username has been changed. | change_username = Your username has been changed. | ||||
| @@ -362,6 +363,7 @@ change_username_prompt = Note: username changes also change your account URL. | |||||
| continue = Continue | continue = Continue | ||||
| cancel = Cancel | cancel = Cancel | ||||
| language = Language | language = Language | ||||
| ui = Theme | |||||
| lookup_avatar_by_mail = Look Up Avatar by Email Address | lookup_avatar_by_mail = Look Up Avatar by Email Address | ||||
| federated_avatar_lookup = Federated Avatar Lookup | federated_avatar_lookup = Federated Avatar Lookup | ||||
| @@ -382,14 +384,18 @@ password_change_disabled = Non-local users can not update their password through | |||||
| emails = Email Addresses | emails = Email Addresses | ||||
| manage_emails = Manage Email Addresses | manage_emails = Manage Email Addresses | ||||
| manage_themes = Select default theme | |||||
| manage_openid = Manage OpenID Addresses | manage_openid = Manage OpenID Addresses | ||||
| email_desc = Your primary email address will be used for notifications and other operations. | email_desc = Your primary email address will be used for notifications and other operations. | ||||
| theme_desc = This will be your default theme across the site. | |||||
| primary = Primary | primary = Primary | ||||
| primary_email = Make Primary | primary_email = Make Primary | ||||
| delete_email = Remove | delete_email = Remove | ||||
| email_deletion = Remove Email Address | email_deletion = Remove Email Address | ||||
| email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue? | email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue? | ||||
| email_deletion_success = The email address has been removed. | email_deletion_success = The email address has been removed. | ||||
| theme_update_success = Your theme was updated. | |||||
| theme_update_error = The selected theme does not exist. | |||||
| openid_deletion = Remove OpenID Address | openid_deletion = Remove OpenID Address | ||||
| openid_deletion_desc = Removing this OpenID address from your account will prevent you from signing in with it. Continue? | openid_deletion_desc = Removing this OpenID address from your account will prevent you from signing in with it. Continue? | ||||
| openid_deletion_success = The OpenID address has been removed. | openid_deletion_success = The OpenID address has been removed. | ||||
| @@ -42,7 +42,7 @@ import ( | |||||
| "github.com/go-macaron/toolbox" | "github.com/go-macaron/toolbox" | ||||
| "github.com/prometheus/client_golang/prometheus" | "github.com/prometheus/client_golang/prometheus" | ||||
| "github.com/tstranex/u2f" | "github.com/tstranex/u2f" | ||||
| "gopkg.in/macaron.v1" | |||||
| macaron "gopkg.in/macaron.v1" | |||||
| ) | ) | ||||
| // NewMacaron initializes Macaron instance. | // NewMacaron initializes Macaron instance. | ||||
| @@ -243,6 +243,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost) | m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost) | ||||
| m.Post("/email/delete", userSetting.DeleteEmail) | m.Post("/email/delete", userSetting.DeleteEmail) | ||||
| m.Post("/delete", userSetting.DeleteAccount) | m.Post("/delete", userSetting.DeleteAccount) | ||||
| m.Post("/theme", bindIgnErr(auth.UpdateThemeForm{}), userSetting.UpdateUIThemePost) | |||||
| }) | }) | ||||
| m.Group("/security", func() { | m.Group("/security", func() { | ||||
| m.Get("", userSetting.Security) | m.Get("", userSetting.Security) | ||||
| @@ -292,6 +293,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| }) | }) | ||||
| }, reqSignIn, func(ctx *context.Context) { | }, reqSignIn, func(ctx *context.Context) { | ||||
| ctx.Data["PageIsUserSettings"] = true | ctx.Data["PageIsUserSettings"] = true | ||||
| ctx.Data["AllThemes"] = setting.UI.Themes | |||||
| }) | }) | ||||
| m.Group("/user", func() { | m.Group("/user", func() { | ||||
| @@ -168,6 +168,34 @@ func DeleteAccount(ctx *context.Context) { | |||||
| } | } | ||||
| } | } | ||||
| // UpdateUIThemePost is used to update users' specific theme | |||||
| func UpdateUIThemePost(ctx *context.Context, form auth.UpdateThemeForm) { | |||||
| ctx.Data["Title"] = ctx.Tr("settings") | |||||
| ctx.Data["PageIsSettingsAccount"] = true | |||||
| if ctx.HasError() { | |||||
| ctx.Redirect(setting.AppSubURL + "/user/settings/account") | |||||
| return | |||||
| } | |||||
| if !form.IsThemeExists() { | |||||
| ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) | |||||
| ctx.Redirect(setting.AppSubURL + "/user/settings/account") | |||||
| return | |||||
| } | |||||
| if err := ctx.User.UpdateTheme(form.Theme); err != nil { | |||||
| ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) | |||||
| ctx.Redirect(setting.AppSubURL + "/user/settings/account") | |||||
| return | |||||
| } | |||||
| log.Trace("Update user theme: %s", ctx.User.Name) | |||||
| ctx.Flash.Success(ctx.Tr("settings.theme_update_success")) | |||||
| ctx.Redirect(setting.AppSubURL + "/user/settings/account") | |||||
| } | |||||
| func loadAccountData(ctx *context.Context) { | func loadAccountData(ctx *context.Context) { | ||||
| emails, err := models.GetEmailAddresses(ctx.User.ID) | emails, err := models.GetEmailAddresses(ctx.User.ID) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -6,7 +6,7 @@ | |||||
| <meta http-equiv="x-ua-compatible" content="ie=edge"> | <meta http-equiv="x-ua-compatible" content="ie=edge"> | ||||
| <title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title> | <title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title> | ||||
| <link rel="manifest" href="{{AppSubUrl}}/manifest.json"> | <link rel="manifest" href="{{AppSubUrl}}/manifest.json"> | ||||
| <script> | <script> | ||||
| if ('serviceWorker' in navigator) { | if ('serviceWorker' in navigator) { | ||||
| window.addEventListener('load', function() { | window.addEventListener('load', function() { | ||||
| @@ -147,7 +147,11 @@ | |||||
| <meta property="og:url" content="{{AppUrl}}" /> | <meta property="og:url" content="{{AppUrl}}" /> | ||||
| <meta property="og:description" content="{{MetaDescription}}"> | <meta property="og:description" content="{{MetaDescription}}"> | ||||
| {{end}} | {{end}} | ||||
| {{if ne DefaultTheme "gitea"}} | |||||
| {{if .IsSigned }} | |||||
| {{ if ne .SignedUser.Theme "gitea" }} | |||||
| <link rel="stylesheet" href="{{AppSubUrl}}/css/theme-{{.SignedUser.Theme}}.css"> | |||||
| {{end}} | |||||
| {{else if ne DefaultTheme "gitea"}} | |||||
| <link rel="stylesheet" href="{{AppSubUrl}}/css/theme-{{DefaultTheme}}.css"> | <link rel="stylesheet" href="{{AppSubUrl}}/css/theme-{{DefaultTheme}}.css"> | ||||
| {{end}} | {{end}} | ||||
| {{template "custom/header" .}} | {{template "custom/header" .}} | ||||
| @@ -32,10 +32,14 @@ var urlsToCache = [ | |||||
| '{{AppSubUrl}}/vendor/plugins/jquery.minicolors/jquery.minicolors.css', | '{{AppSubUrl}}/vendor/plugins/jquery.minicolors/jquery.minicolors.css', | ||||
| '{{AppSubUrl}}/vendor/plugins/jquery.datetimepicker/jquery.datetimepicker.css', | '{{AppSubUrl}}/vendor/plugins/jquery.datetimepicker/jquery.datetimepicker.css', | ||||
| '{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.css', | '{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.css', | ||||
| {{if ne DefaultTheme "gitea"}} | |||||
| '{{AppSubUrl}}/css/theme-{{DefaultTheme}}.css', | |||||
| {{if .IsSigned }} | |||||
| {{ if ne .SignedUser.Theme "gitea" }} | |||||
| '{{AppSubUrl}}/css/theme-{{.SignedUser.Theme}}.css' | |||||
| {{end}} | |||||
| {{else if ne DefaultTheme "gitea"}} | |||||
| '{{AppSubUrl}}/css/theme-{{DefaultTheme}}.css' | |||||
| {{end}} | {{end}} | ||||
| // img | // img | ||||
| '{{AppSubUrl}}/img/gitea-sm.png', | '{{AppSubUrl}}/img/gitea-sm.png', | ||||
| '{{AppSubUrl}}/img/gitea-lg.png', | '{{AppSubUrl}}/img/gitea-lg.png', | ||||
| @@ -85,6 +85,44 @@ | |||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| <h4 class="ui top attached header"> | |||||
| {{.i18n.Tr "settings.manage_themes"}} | |||||
| </h4> | |||||
| <div class="ui attached segment"> | |||||
| <div class="ui email list"> | |||||
| <div class="item"> | |||||
| {{.i18n.Tr "settings.theme_desc"}} | |||||
| </div> | |||||
| <form class="ui form" action="{{.Link}}/theme" method="post"> | |||||
| {{.CsrfTokenHtml}} | |||||
| <div class="field"> | |||||
| <label for="ui">{{.i18n.Tr "settings.ui"}}</label> | |||||
| <div class="ui selection dropdown" id="ui"> | |||||
| <input name="theme" type="hidden" value="{{.SignedUser.Theme}}"> | |||||
| <i class="dropdown icon"></i> | |||||
| <div class="text"> | |||||
| {{range $i,$a := .AllThemes}} | |||||
| {{if eq $.SignedUser.Theme $a}}{{$a}}{{end}} | |||||
| {{end}} | |||||
| </div> | |||||
| <div class="menu"> | |||||
| {{range $i,$a := .AllThemes}} | |||||
| <div class="item{{if eq $.SignedUser.Theme $a}} active selected{{end}}" data-value="{{$a}}"> | |||||
| {{$a}} | |||||
| </div> | |||||
| {{end}} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="field"> | |||||
| <button class="ui green button">{{$.i18n.Tr "settings.update_theme"}}</button> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| <h4 class="ui top attached warning header"> | <h4 class="ui top attached warning header"> | ||||
| {{.i18n.Tr "settings.delete_account"}} | {{.i18n.Tr "settings.delete_account"}} | ||||
| </h4> | </h4> | ||||