Allow for user specific themes (#5668)
* 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 review
This commit is contained in:
		
							parent
							
								
									ea518681d9
								
							
						
					
					
						commit
						8d2c24f7f9
					
				
					 14 changed files with 157 additions and 11 deletions
				
			
		| 
						 | 
				
			
			@ -340,6 +340,7 @@ func runCreateUser(c *cli.Context) error {
 | 
			
		|||
		IsActive:           true,
 | 
			
		||||
		IsAdmin:            c.Bool("admin"),
 | 
			
		||||
		MustChangePassword: changePassword,
 | 
			
		||||
		Theme:              setting.UI.DefaultTheme,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return fmt.Errorf("CreateUser: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,6 +85,8 @@ MAX_DISPLAY_FILE_SIZE = 8388608
 | 
			
		|||
SHOW_USER_EMAIL = true
 | 
			
		||||
; Set the default theme for the Gitea install
 | 
			
		||||
DEFAULT_THEME = gitea
 | 
			
		||||
; All available themes
 | 
			
		||||
THEMES = gitea,arc-green
 | 
			
		||||
 | 
			
		||||
[ui.admin]
 | 
			
		||||
; Number of users that are displayed on one page
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ import (
 | 
			
		|||
	"github.com/Unknwon/com"
 | 
			
		||||
	"github.com/go-xorm/xorm"
 | 
			
		||||
	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/log"
 | 
			
		||||
| 
						 | 
				
			
			@ -206,6 +206,8 @@ var migrations = []Migration{
 | 
			
		|||
	NewMigration("clear nonused data which not deleted when user was deleted", clearNonusedData),
 | 
			
		||||
	// v76 -> v77
 | 
			
		||||
	NewMigration("add pull request rebase with merge commit", addPullRequestRebaseWithMerge),
 | 
			
		||||
	// v77 -> v78
 | 
			
		||||
	NewMigration("add theme to users", addUserDefaultTheme),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Migrate database to current version
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								models/migrations/v77.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								models/migrations/v77.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
	DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
 | 
			
		||||
	Theme         string `xorm:"NOT NULL DEFAULT ''"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BeforeUpdate is invoked from XORM before updating this object.
 | 
			
		||||
| 
						 | 
				
			
			@ -165,6 +166,13 @@ func (u *User) BeforeUpdate() {
 | 
			
		|||
	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
 | 
			
		||||
func (u *User) SetLastLogin() {
 | 
			
		||||
	u.LastLoginUnix = util.TimeStampNow()
 | 
			
		||||
| 
						 | 
				
			
			@ -176,6 +184,12 @@ func (u *User) UpdateDiffViewStyle(style string) error {
 | 
			
		|||
	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
 | 
			
		||||
// email address private, otherwise the primary email address.
 | 
			
		||||
func (u *User) getEmail() string {
 | 
			
		||||
| 
						 | 
				
			
			@ -777,6 +791,7 @@ func CreateUser(u *User) (err error) {
 | 
			
		|||
	u.HashPassword(u.Passwd)
 | 
			
		||||
	u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
 | 
			
		||||
	u.MaxRepoCreation = -1
 | 
			
		||||
	u.Theme = setting.UI.DefaultTheme
 | 
			
		||||
 | 
			
		||||
	if _, err = sess.Insert(u); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-macaron/binding"
 | 
			
		||||
	"gopkg.in/macaron.v1"
 | 
			
		||||
	macaron "gopkg.in/macaron.v1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
type ChangePasswordForm struct {
 | 
			
		||||
	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", ",")
 | 
			
		||||
	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]", ",")
 | 
			
		||||
	defaultThemes                            = strings.Split("gitea", "arc-green")
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,9 +33,9 @@ import (
 | 
			
		|||
	"github.com/go-macaron/session"
 | 
			
		||||
	_ "github.com/go-macaron/session/redis" // redis plugin for store session
 | 
			
		||||
	"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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -303,6 +303,7 @@ var (
 | 
			
		|||
		MaxDisplayFileSize  int64
 | 
			
		||||
		ShowUserEmail       bool
 | 
			
		||||
		DefaultTheme        string
 | 
			
		||||
		Themes              []string
 | 
			
		||||
 | 
			
		||||
		Admin struct {
 | 
			
		||||
			UserPagingNum   int
 | 
			
		||||
| 
						 | 
				
			
			@ -329,6 +330,7 @@ var (
 | 
			
		|||
		ThemeColorMetaTag:   `#6cc644`,
 | 
			
		||||
		MaxDisplayFileSize:  8388608,
 | 
			
		||||
		DefaultTheme:        `gitea`,
 | 
			
		||||
		Themes:              []string{`gitea`, `arc-green`},
 | 
			
		||||
		Admin: struct {
 | 
			
		||||
			UserPagingNum   int
 | 
			
		||||
			RepoPagingNum   int
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -355,6 +355,7 @@ password_username_disabled = Non-local users are not allowed to change their use
 | 
			
		|||
full_name = Full Name
 | 
			
		||||
website = Website
 | 
			
		||||
location = Location
 | 
			
		||||
update_theme = Update Theme
 | 
			
		||||
update_profile = Update Profile
 | 
			
		||||
update_profile_success = Your profile has been updated.
 | 
			
		||||
change_username = Your username has been changed.
 | 
			
		||||
| 
						 | 
				
			
			@ -362,6 +363,7 @@ change_username_prompt = Note: username changes also change your account URL.
 | 
			
		|||
continue = Continue
 | 
			
		||||
cancel = Cancel
 | 
			
		||||
language = Language
 | 
			
		||||
ui = Theme
 | 
			
		||||
 | 
			
		||||
lookup_avatar_by_mail = Look Up Avatar by Email Address
 | 
			
		||||
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
 | 
			
		||||
manage_emails = Manage Email Addresses
 | 
			
		||||
manage_themes = Select default theme
 | 
			
		||||
manage_openid = Manage OpenID Addresses
 | 
			
		||||
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_email = Make Primary
 | 
			
		||||
delete_email = Remove
 | 
			
		||||
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_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_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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ import (
 | 
			
		|||
	"github.com/go-macaron/toolbox"
 | 
			
		||||
	"github.com/prometheus/client_golang/prometheus"
 | 
			
		||||
	"github.com/tstranex/u2f"
 | 
			
		||||
	"gopkg.in/macaron.v1"
 | 
			
		||||
	macaron "gopkg.in/macaron.v1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// NewMacaron initializes Macaron instance.
 | 
			
		||||
| 
						 | 
				
			
			@ -243,6 +243,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
			
		|||
			m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost)
 | 
			
		||||
			m.Post("/email/delete", userSetting.DeleteEmail)
 | 
			
		||||
			m.Post("/delete", userSetting.DeleteAccount)
 | 
			
		||||
			m.Post("/theme", bindIgnErr(auth.UpdateThemeForm{}), userSetting.UpdateUIThemePost)
 | 
			
		||||
		})
 | 
			
		||||
		m.Group("/security", func() {
 | 
			
		||||
			m.Get("", userSetting.Security)
 | 
			
		||||
| 
						 | 
				
			
			@ -292,6 +293,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
			
		|||
		})
 | 
			
		||||
	}, reqSignIn, func(ctx *context.Context) {
 | 
			
		||||
		ctx.Data["PageIsUserSettings"] = true
 | 
			
		||||
		ctx.Data["AllThemes"] = setting.UI.Themes
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	emails, err := models.GetEmailAddresses(ctx.User.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
	<meta http-equiv="x-ua-compatible" content="ie=edge">
 | 
			
		||||
	<title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title>
 | 
			
		||||
	<link rel="manifest" href="{{AppSubUrl}}/manifest.json">
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
	<script>
 | 
			
		||||
		if ('serviceWorker' in navigator) {
 | 
			
		||||
  			window.addEventListener('load', function() {
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +147,11 @@
 | 
			
		|||
	<meta property="og:url" content="{{AppUrl}}" />
 | 
			
		||||
	<meta property="og:description" content="{{MetaDescription}}">
 | 
			
		||||
{{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">
 | 
			
		||||
{{end}}
 | 
			
		||||
{{template "custom/header" .}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,10 +32,14 @@ var urlsToCache = [
 | 
			
		|||
  '{{AppSubUrl}}/vendor/plugins/jquery.minicolors/jquery.minicolors.css',
 | 
			
		||||
  '{{AppSubUrl}}/vendor/plugins/jquery.datetimepicker/jquery.datetimepicker.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}}
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // img
 | 
			
		||||
  '{{AppSubUrl}}/img/gitea-sm.png',
 | 
			
		||||
  '{{AppSubUrl}}/img/gitea-lg.png',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,6 +85,44 @@
 | 
			
		|||
			</form>
 | 
			
		||||
		</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">
 | 
			
		||||
			{{.i18n.Tr "settings.delete_account"}}
 | 
			
		||||
		</h4>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue