custom avatar upload

This commit is contained in:
Unknwon 2014-11-21 10:58:08 -05:00
parent 3c3f7c2a56
commit 55dfe2c978
20 changed files with 239 additions and 97 deletions

View file

@ -71,7 +71,6 @@ There are 5 ways to install Gogs:
- Router and middleware mechanism of [Macaron](https://github.com/Unknwon/macaron). - Router and middleware mechanism of [Macaron](https://github.com/Unknwon/macaron).
- Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk). - Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk).
- System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog). - System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog).
- Usage and modification from [beego](http://beego.me) modules.
- Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo. - Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo.
- Thanks [gobuild.io](http://gobuild.io) for providing binary compile and download service. - Thanks [gobuild.io](http://gobuild.io) for providing binary compile and download service.
- Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan. - Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan.

View file

@ -59,8 +59,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
## 特别鸣谢 ## 特别鸣谢
- [Macaron](https://github.com/Unknwon/macaron) 的路由与中间件机制。 - 基于 [Macaron](https://github.com/Unknwon/macaron) 的路由与中间件机制。
- [beego](http://beego.me) 模块的使用与修改。
- 基于 [WeTalk](https://github.com/beego/wetalk) 修改的邮件服务和模块设计。 - 基于 [WeTalk](https://github.com/beego/wetalk) 修改的邮件服务和模块设计。
- 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。 - 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。
- 感谢 [gobuild.io](http://gobuild.io) 提供二进制编译与下载服务。 - 感谢 [gobuild.io](http://gobuild.io) 提供二进制编译与下载服务。

View file

@ -94,6 +94,13 @@ func newMacaron() *macaron.Macaron {
SkipLogging: !setting.DisableRouterLog, SkipLogging: !setting.DisableRouterLog,
}, },
)) ))
m.Use(macaron.Static(
setting.AvatarUploadPath,
macaron.StaticOptions{
Prefix: "avatars",
SkipLogging: !setting.DisableRouterLog,
},
))
m.Use(macaron.Renderer(macaron.RenderOptions{ m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: path.Join(setting.StaticRootPath, "templates"), Directory: path.Join(setting.StaticRootPath, "templates"),
Funcs: []template.FuncMap{base.TemplateFuncs}, Funcs: []template.FuncMap{base.TemplateFuncs},
@ -214,6 +221,7 @@ func runWeb(*cli.Context) {
m.Group("/user/settings", func() { m.Group("/user/settings", func() {
m.Get("", user.Settings) m.Get("", user.Settings)
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost) m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost)
m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), user.SettingsAvatar)
m.Get("/password", user.SettingsPassword) m.Get("/password", user.SettingsPassword)
m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost) m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
m.Get("/ssh", user.SettingsSSHKeys) m.Get("/ssh", user.SettingsSSHKeys)

View file

@ -167,6 +167,7 @@ SESSION_LIFE_TIME = 86400
[picture] [picture]
; The place to picture data, either "server" or "qiniu", default is "server" ; The place to picture data, either "server" or "qiniu", default is "server"
SERVICE = server SERVICE = server
AVATAR_UPLOAD_PATH = data/avatars
; Chinese users can choose "duoshuo" ; Chinese users can choose "duoshuo"
GRAVATAR_SOURCE = gravatar GRAVATAR_SOURCE = gravatar
DISABLE_GRAVATAR = false DISABLE_GRAVATAR = false

View file

@ -173,6 +173,7 @@ target_branch_not_exist = Target branch does not exist
[user] [user]
change_avatar = Change your avatar at gravatar.com change_avatar = Change your avatar at gravatar.com
change_custom_avatar = Change your avatar in settings
join_on = Joined on join_on = Joined on
repositories = Repositories repositories = Repositories
activity = Public Activity activity = Public Activity
@ -201,6 +202,10 @@ change_username = Username Changed
change_username_desc = Username has been changed, do you want to continue? This will affect all links relate to your account. change_username_desc = Username has been changed, do you want to continue? This will affect all links relate to your account.
continue = Continue continue = Continue
cancel = Cancel cancel = Cancel
choose_new_avatar = Choose new avatar
upload_avatar = Upload Avatar
uploaded_avatar_not_a_image = Uploaded file is not a image
upload_avatar_success = Your new avatar has been uploaded successfully.
change_password = Change Password change_password = Change Password
old_password = Current Password old_password = Current Password

View file

@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
) )
const APP_VER = "0.5.8.1119 Beta" const APP_VER = "0.5.8.1121 Beta"
func init() { func init() {
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())

View file

@ -58,6 +58,7 @@ type Action struct {
ActUserId int64 // Action user id. ActUserId int64 // Action user id.
ActUserName string // Action user name. ActUserName string // Action user name.
ActEmail string ActEmail string
ActAvatar string `xorm:"-"`
RepoId int64 RepoId int64
RepoUserName string RepoUserName string
RepoName string RepoName string

View file

@ -5,17 +5,21 @@
package models package models
import ( import (
"bytes"
"container/list" "container/list"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"image"
"image/jpeg"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"github.com/nfnt/resize"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/git"
@ -45,33 +49,40 @@ var (
// User represents the object of individual and member of organization. // User represents the object of individual and member of organization.
type User struct { type User struct {
Id int64 Id int64
LowerName string `xorm:"UNIQUE NOT NULL"` LowerName string `xorm:"UNIQUE NOT NULL"`
Name string `xorm:"UNIQUE NOT NULL"` Name string `xorm:"UNIQUE NOT NULL"`
FullName string FullName string
Email string `xorm:"UNIQUE NOT NULL"` Email string `xorm:"UNIQUE NOT NULL"`
Passwd string `xorm:"NOT NULL"` Passwd string `xorm:"NOT NULL"`
LoginType LoginType LoginType LoginType
LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`
LoginName string LoginName string
Type UserType Type UserType
Orgs []*User `xorm:"-"` Orgs []*User `xorm:"-"`
Repos []*Repository `xorm:"-"` Repos []*Repository `xorm:"-"`
Location string
Website string
Rands string `xorm:"VARCHAR(10)"`
Salt string `xorm:"VARCHAR(10)"`
Created time.Time `xorm:"CREATED"`
Updated time.Time `xorm:"UPDATED"`
// Permissions.
IsActive bool
IsAdmin bool
AllowGitHook bool
// Avatar.
Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
AvatarEmail string `xorm:"NOT NULL"`
UseCustomAvatar bool
// Counters.
NumFollowers int NumFollowers int
NumFollowings int NumFollowings int
NumStars int NumStars int
NumRepos int NumRepos int
Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
AvatarEmail string `xorm:"NOT NULL"`
Location string
Website string
IsActive bool
IsAdmin bool
AllowGitHook bool
Rands string `xorm:"VARCHAR(10)"`
Salt string `xorm:"VARCHAR(10)"`
Created time.Time `xorm:"CREATED"`
Updated time.Time `xorm:"UPDATED"`
// For organization. // For organization.
Description string Description string
@ -96,9 +107,12 @@ func (u *User) HomeLink() string {
// AvatarLink returns user gravatar link. // AvatarLink returns user gravatar link.
func (u *User) AvatarLink() string { func (u *User) AvatarLink() string {
if setting.DisableGravatar { switch {
case u.UseCustomAvatar:
return setting.AppSubUrl + "/avatars/" + com.ToStr(u.Id)
case setting.DisableGravatar:
return setting.AppSubUrl + "/img/avatar_default.jpg" return setting.AppSubUrl + "/img/avatar_default.jpg"
} else if setting.Service.EnableCacheAvatar { case setting.Service.EnableCacheAvatar:
return setting.AppSubUrl + "/avatar/" + u.Avatar return setting.AppSubUrl + "/avatar/" + u.Avatar
} }
return setting.GravatarSource + u.Avatar return setting.GravatarSource + u.Avatar
@ -126,6 +140,43 @@ func (u *User) ValidtePassword(passwd string) bool {
return u.Passwd == newUser.Passwd return u.Passwd == newUser.Passwd
} }
// UploadAvatar saves custom avatar for user.
// FIXME: splite uploads to different subdirs in case we have massive users.
func (u *User) UploadAvatar(data []byte) error {
savePath := filepath.Join(setting.AvatarUploadPath, com.ToStr(u.Id))
u.UseCustomAvatar = true
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return err
}
m := resize.Resize(200, 200, img, resize.NearestNeighbor)
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Id(u.Id).AllCols().Update(u); err != nil {
sess.Rollback()
return err
}
fw, err := os.Create(savePath)
if err != nil {
sess.Rollback()
return err
}
defer fw.Close()
if err = jpeg.Encode(fw, m, nil); err != nil {
sess.Rollback()
return err
}
return sess.Commit()
}
// IsOrganization returns true if user is actually a organization. // IsOrganization returns true if user is actually a organization.
func (u *User) IsOrganization() bool { func (u *User) IsOrganization() bool {
return u.Type == ORGANIZATION return u.Type == ORGANIZATION
@ -517,41 +568,38 @@ func GetUserIdsByNames(names []string) []int64 {
// UserCommit represtns a commit with validation of user. // UserCommit represtns a commit with validation of user.
type UserCommit struct { type UserCommit struct {
UserName string User *User
*git.Commit *git.Commit
} }
// ValidateCommitWithEmail chceck if author's e-mail of commit is corresponsind to a user. // ValidateCommitWithEmail chceck if author's e-mail of commit is corresponsind to a user.
func ValidateCommitWithEmail(c *git.Commit) (uname string) { func ValidateCommitWithEmail(c *git.Commit) *User {
u, err := GetUserByEmail(c.Author.Email) u, err := GetUserByEmail(c.Author.Email)
if err == nil { if err != nil {
uname = u.Name return nil
} }
return uname return u
} }
// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users. // ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
func ValidateCommitsWithEmails(oldCommits *list.List) *list.List { func ValidateCommitsWithEmails(oldCommits *list.List) *list.List {
emails := map[string]string{} emails := map[string]*User{}
newCommits := list.New() newCommits := list.New()
e := oldCommits.Front() e := oldCommits.Front()
for e != nil { for e != nil {
c := e.Value.(*git.Commit) c := e.Value.(*git.Commit)
uname := "" var u *User
if v, ok := emails[c.Author.Email]; !ok { if v, ok := emails[c.Author.Email]; !ok {
u, err := GetUserByEmail(c.Author.Email) u, _ = GetUserByEmail(c.Author.Email)
if err == nil { emails[c.Author.Email] = u
uname = u.Name
}
emails[c.Author.Email] = uname
} else { } else {
uname = v u = v
} }
newCommits.PushBack(UserCommit{ newCommits.PushBack(UserCommit{
UserName: uname, User: u,
Commit: c, Commit: c,
}) })
e = e.Next() e = e.Next()
} }

View file

@ -5,6 +5,8 @@
package auth package auth
import ( import (
"mime/multipart"
"github.com/Unknwon/macaron" "github.com/Unknwon/macaron"
"github.com/macaron-contrib/binding" "github.com/macaron-contrib/binding"
) )
@ -86,6 +88,14 @@ func (f *UpdateProfileForm) Validate(ctx *macaron.Context, errs binding.Errors)
return validate(errs, ctx.Data, f, ctx.Locale) return validate(errs, ctx.Data, f, ctx.Locale)
} }
type UploadAvatarForm struct {
Avatar *multipart.FileHeader `form:"avatar" binding:"Required"`
}
func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
type ChangePasswordForm struct { type ChangePasswordForm struct {
OldPassword string `form:"old_password" binding:"Required;MinSize(6);MaxSize(255)"` OldPassword string `form:"old_password" binding:"Required;MinSize(6);MaxSize(255)"`
Password string `form:"password" binding:"Required;MinSize(6);MaxSize(255)"` Password string `form:"password" binding:"Required;MinSize(6);MaxSize(255)"`

View file

@ -121,7 +121,7 @@ func (this *Avatar) Encode(wr io.Writer, size int) (err error) {
if img, err = decodeImageFile(imgPath); err != nil { if img, err = decodeImageFile(imgPath); err != nil {
return return
} }
m := resize.Resize(uint(size), 0, img, resize.Lanczos3) m := resize.Resize(uint(size), 0, img, resize.NearestNeighbor)
return jpeg.Encode(wr, m, nil) return jpeg.Encode(wr, m, nil)
} }

View file

@ -66,9 +66,10 @@ var (
ScriptType string ScriptType string
// Picture settings. // Picture settings.
PictureService string PictureService string
GravatarSource string AvatarUploadPath string
DisableGravatar bool GravatarSource string
DisableGravatar bool
// Log settings. // Log settings.
LogRootPath string LogRootPath string
@ -259,6 +260,9 @@ func NewConfigContext() {
ScriptType = Cfg.MustValue("repository", "SCRIPT_TYPE", "bash") ScriptType = Cfg.MustValue("repository", "SCRIPT_TYPE", "bash")
PictureService = Cfg.MustValueRange("picture", "SERVICE", "server", []string{"server"}) PictureService = Cfg.MustValueRange("picture", "SERVICE", "server", []string{"server"})
AvatarUploadPath = Cfg.MustValue("picture", "AVATAR_UPLOAD_PATH", "data/avatars")
os.MkdirAll(AvatarUploadPath, os.ModePerm)
switch Cfg.MustValue("picture", "GRAVATAR_SOURCE", "gravatar") { switch Cfg.MustValue("picture", "GRAVATAR_SOURCE", "gravatar") {
case "duoshuo": case "duoshuo":
GravatarSource = "http://gravatar.duoshuo.com/avatar/" GravatarSource = "http://gravatar.duoshuo.com/avatar/"

View file

@ -100,6 +100,13 @@ func Dashboard(ctx *middleware.Context) {
continue continue
} }
} }
// FIXME: cache results?
u, err := models.GetUserByName(act.ActUserName)
if err != nil {
ctx.Handle(500, "GetUserByName", err)
return
}
act.ActAvatar = u.AvatarLink()
feeds = append(feeds, act) feeds = append(feeds, act)
} }
ctx.Data["Feeds"] = feeds ctx.Data["Feeds"] = feeds

View file

@ -5,6 +5,7 @@
package user package user
import ( import (
"io/ioutil"
"strings" "strings"
"github.com/Unknwon/com" "github.com/Unknwon/com"
@ -83,6 +84,34 @@ func SettingsPost(ctx *middleware.Context, form auth.UpdateProfileForm) {
ctx.Redirect(setting.AppSubUrl + "/user/settings") ctx.Redirect(setting.AppSubUrl + "/user/settings")
} }
// FIXME: limit size.
func SettingsAvatar(ctx *middleware.Context, form auth.UploadAvatarForm) {
defer ctx.Redirect(setting.AppSubUrl + "/user/settings")
if form.Avatar != nil {
fr, err := form.Avatar.Open()
if err != nil {
ctx.Flash.Error(err.Error())
return
}
data, err := ioutil.ReadAll(fr)
if err != nil {
ctx.Flash.Error(err.Error())
return
}
if _, ok := base.IsImageFile(data); !ok {
ctx.Flash.Error(ctx.Tr("settings.uploaded_avatar_not_a_image"))
return
}
if err = ctx.User.UploadAvatar(data); err != nil {
ctx.Flash.Error(err.Error())
return
}
ctx.Flash.Success(ctx.Tr("settings.upload_avatar_success"))
}
}
func SettingsPassword(ctx *middleware.Context) { func SettingsPassword(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsUserSettings"] = true ctx.Data["PageIsUserSettings"] = true

View file

@ -1 +1 @@
0.5.8.1119 Beta 0.5.8.1121 Beta

View file

@ -24,7 +24,13 @@
{{$r := List .Commits}} {{$r := List .Commits}}
{{range $r}} {{range $r}}
<tr> <tr>
<td class="author"><img class="avatar-20" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;&nbsp;{{if .UserName}}<a href="{{AppSubUrl}}/{{.UserName}}">{{.Author.Name}}</a>{{else}}{{.Author.Name}}{{end}}</td> <td class="author">
{{if .User}}
<img class="avatar-20" src="{{.User.AvatarLink}}" alt=""/>&nbsp;&nbsp;&nbsp;<a href="{{AppSubUrl}}/{{.User.Name}}">{{.Author.Name}}</a>
{{else}}
<img class="avatar-20" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;&nbsp;{{.Author.Name}}
{{end}}
</td>
<td class="sha"><a rel="nofollow" class="label label-green" href="{{AppSubUrl}}/{{$username}}/{{$reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </a></td> <td class="sha"><a rel="nofollow" class="label label-green" href="{{AppSubUrl}}/{{$username}}/{{$reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </a></td>
<td class="message"><span class="text-truncate">{{.Summary}}</span></td> <td class="message"><span class="text-truncate">{{.Summary}}</span></td>
<td class="date">{{TimeSince .Author.When $.Lang}}</td> <td class="date">{{TimeSince .Author.When $.Lang}}</td>

View file

@ -30,10 +30,11 @@
</ul> </ul>
</span> </span>
<p class="author"> <p class="author">
<img class="avatar-30" src="{{AvatarLink .Commit.Author.Email}}" />
{{if .Author}} {{if .Author}}
<a href="{{AppSubUrl}}/{{.Author}}"><strong>{{.Commit.Author.Name}}</strong></a> <img class="avatar-30" src="{{.Author.AvatarLink}}" />
<a href="{{AppSubUrl}}/{{.Author.Name}}"><strong>{{.Commit.Author.Name}}</strong></a>
{{else}} {{else}}
<img class="avatar-30" src="{{AvatarLink .Commit.Author.Email}}" />
<strong>{{.Commit.Author.Name}}</strong> <strong>{{.Commit.Author.Name}}</strong>
{{end}} {{end}}
<span class="text-grey" id="authored-time">{{TimeSince .Commit.Author.When $.Lang}}</span> <span class="text-grey" id="authored-time">{{TimeSince .Commit.Author.When $.Lang}}</span>

View file

@ -3,8 +3,14 @@
<tr> <tr>
<th colspan="4" class="clear"> <th colspan="4" class="clear">
<span class="author left"> <span class="author left">
{{if .LastCommitUser}}
<img class="avatar-24 radius" src="{{.LastCommitUser.AvatarLink}}" />
<a href="{{AppSubUrl}}/{{.LastCommitUser.Name}}"><strong>{{.LastCommit.Author.Name}}</strong></a>:
{{else}}
<img class="avatar-24 radius" src="{{AvatarLink .LastCommit.Author.Email}}" /> <img class="avatar-24 radius" src="{{AvatarLink .LastCommit.Author.Email}}" />
{{if .LastCommitUser}}<a href="{{AppSubUrl}}/{{.LastCommitUser}}">{{end}}<strong>{{.LastCommit.Author.Name}}</strong>:{{if .LastCommitUser}}</a>{{end}} <strong>{{.LastCommit.Author.Name}}</strong>:
{{end}}
&nbsp;
</span> </span>
<span class="last-commit"><a href="{{.RepoLink}}/commit/{{.LastCommit.Id}}" rel="nofollow"> <span class="last-commit"><a href="{{.RepoLink}}/commit/{{.LastCommit.Id}}" rel="nofollow">
<strong>{{ShortSha .LastCommit.Id.String}}</strong></a> <strong>{{ShortSha .LastCommit.Id.String}}</strong></a>

View file

@ -1,7 +1,7 @@
{{range .Feeds}} {{range .Feeds}}
<div class="news clear"> <div class="news clear">
<div class="avatar left"> <div class="avatar left">
<img class="avatar-30" src="{{AvatarLink .GetActEmail}}" alt=""> <img class="avatar-30" src="{{.ActAvatar}}" alt="">
</div> </div>
<div class="content left {{if eq .GetOpType 5}}push-news{{end}} grid-4-5"> <div class="content left {{if eq .GetOpType 5}}push-news{{end}} grid-4-5">
<p class="text-bold"> <p class="text-bold">

View file

@ -4,7 +4,11 @@
<div id="user-profile-page" class="container clear"> <div id="user-profile-page" class="container clear">
<div class="grid-1-5 left"> <div class="grid-1-5 left">
<div> <div>
{{if .Owner.UseCustomAvatar}}
<a href="{{AppSubUrl}}/user/settings" id="profile-avatar" original-title="{{.i18n.Tr "user.change_custom_avatar"}}">
{{else}}
<a href="http://gravatar.com/emails/" id="profile-avatar" original-title="{{.i18n.Tr "user.change_avatar"}}"> <a href="http://gravatar.com/emails/" id="profile-avatar" original-title="{{.i18n.Tr "user.change_avatar"}}">
{{end}}
<img class="profile-avatar" src="{{.Owner.AvatarLink}}?s=200"title="{{.Owner.Name}}"/> <img class="profile-avatar" src="{{.Owner.AvatarLink}}?s=200"title="{{.Owner.Name}}"/>
</a> </a>
<div class="text-center" id="profile-name"> <div class="text-center" id="profile-name">

View file

@ -11,49 +11,63 @@
<div class="panel-header"> <div class="panel-header">
<strong>{{.i18n.Tr "settings.public_profile"}}</strong> <strong>{{.i18n.Tr "settings.public_profile"}}</strong>
</div> </div>
<form class="form form-align panel-body" id="user-profile-form" action="{{AppSubUrl}}/user/settings" method="post"> <div class="panel-body">
{{.CsrfTokenHtml}} <form class="form form-align" id="user-profile-form" action="{{AppSubUrl}}/user/settings" method="post">
<div class="text-center panel-desc">{{.i18n.Tr "settings.profile_desc"}}</div> {{.CsrfTokenHtml}}
<div class="field"> <div class="text-center panel-desc">{{.i18n.Tr "settings.profile_desc"}}</div>
<label>{{.i18n.Tr "settings.uid"}}</label> <div class="field">
<label class="text-left">{{.SignedUser.Id}}</label> <label>{{.i18n.Tr "settings.uid"}}</label>
</div> <label class="text-left">{{.SignedUser.Id}}</label>
<div class="field"> </div>
<label class="req" for="username">{{.i18n.Tr "username"}}</label> <div class="field">
<input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="username" name="uname" type="text" value="{{.SignedUser.Name}}" data-uname="{{.SignedUser.Name}}" required /> <label class="req" for="username">{{.i18n.Tr "username"}}</label>
</div> <input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="username" name="uname" type="text" value="{{.SignedUser.Name}}" data-uname="{{.SignedUser.Name}}" required />
<div class="white-popup-block mfp-hide" id="change-username-modal"> </div>
<h1 class="text-red">{{.i18n.Tr "settings.change_username"}}</h1> <div class="white-popup-block mfp-hide" id="change-username-modal">
<p>{{.i18n.Tr "settings.change_username_desc"}}</p> <h1 class="text-red">{{.i18n.Tr "settings.change_username"}}</h1>
<br> <p>{{.i18n.Tr "settings.change_username_desc"}}</p>
<button class="btn btn-red btn-large btn-radius" id="change-username-submit">{{.i18n.Tr "settings.continue"}}</button> <br>
<button class="btn btn-large btn-radius popup-modal-dismiss">{{.i18n.Tr "settings.cancel"}}</button> <button class="btn btn-red btn-large btn-radius" id="change-username-submit">{{.i18n.Tr "settings.continue"}}</button>
</div> <button class="btn btn-large btn-radius popup-modal-dismiss">{{.i18n.Tr "settings.cancel"}}</button>
<div class="field"> </div>
<label for="full-name">{{.i18n.Tr "settings.full_name"}}</label> <div class="field">
<input class="ipt ipt-large ipt-radius {{if .Err_FullName}}ipt-error{{end}}" id="full-name" name="fullname" type="text" value="{{.SignedUser.FullName}}" /> <label for="full-name">{{.i18n.Tr "settings.full_name"}}</label>
</div> <input class="ipt ipt-large ipt-radius {{if .Err_FullName}}ipt-error{{end}}" id="full-name" name="fullname" type="text" value="{{.SignedUser.FullName}}" />
<div class="field"> </div>
<label class="req" for="email">{{.i18n.Tr "email"}}</label> <div class="field">
<input class="ipt ipt-large ipt-radius {{if .Err_Email}}ipt-error{{end}}" id="email" name="email" type="email" value="{{.SignedUser.Email}}" required /> <label class="req" for="email">{{.i18n.Tr "email"}}</label>
</div> <input class="ipt ipt-large ipt-radius {{if .Err_Email}}ipt-error{{end}}" id="email" name="email" type="email" value="{{.SignedUser.Email}}" required />
<div class="field"> </div>
<label for="website">{{.i18n.Tr "settings.website"}}</label> <div class="field">
<input class="ipt ipt-large ipt-radius {{if .Err_Website}}ipt-error{{end}}" id="website" name="website" type="url" value="{{.SignedUser.Website}}" /> <label for="website">{{.i18n.Tr "settings.website"}}</label>
</div> <input class="ipt ipt-large ipt-radius {{if .Err_Website}}ipt-error{{end}}" id="website" name="website" type="url" value="{{.SignedUser.Website}}" />
<div class="field"> </div>
<label for="location">{{.i18n.Tr "settings.location"}}</label> <div class="field">
<input class="ipt ipt-large ipt-radius {{if .Err_Location}}ipt-error{{end}}" id="location" name="location" type="text" value="{{.SignedUser.Location}}" /> <label for="location">{{.i18n.Tr "settings.location"}}</label>
</div> <input class="ipt ipt-large ipt-radius {{if .Err_Location}}ipt-error{{end}}" id="location" name="location" type="text" value="{{.SignedUser.Location}}" />
<div class="field"> </div>
<label for="gravatar-email">Gravatar {{.i18n.Tr "email"}}</label> <div class="field">
<input class="ipt ipt-large ipt-radius {{if .Err_Avatar}}ipt-error{{end}}" id="gravatar-email" name="avatar" type="text" value="{{.SignedUser.AvatarEmail}}" /> <label for="gravatar-email">Gravatar {{.i18n.Tr "email"}}</label>
</div> <input class="ipt ipt-large ipt-radius {{if .Err_Avatar}}ipt-error{{end}}" id="gravatar-email" name="avatar" type="text" value="{{.SignedUser.AvatarEmail}}" />
<div class="field"> </div>
<label></label> <div class="field">
<button class="btn btn-green btn-large btn-radius" id="change-username-btn" href="#change-username-modal">{{.i18n.Tr "settings.update_profile"}}</button> <label></label>
</div> <button class="btn btn-green btn-large btn-radius" id="change-username-btn" href="#change-username-modal">{{.i18n.Tr "settings.update_profile"}}</button>
</form> </div>
</form>
<hr>
<form class="form form-align" id="user-profile-form" action="{{AppSubUrl}}/user/settings/avatar" method="post" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<div class="field">
<label>{{.i18n.Tr "settings.choose_new_avatar"}}</label>
<input name="avatar" type="file" required />
</div>
<div class="field">
<label></label>
<button class="btn btn-green btn-large btn-radius">{{.i18n.Tr "settings.upload_avatar"}}</button>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>