diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | cmd/web.go | 10 | ||||
-rw-r--r-- | conf/app.ini | 2 | ||||
-rw-r--r-- | models/repo.go | 62 | ||||
-rw-r--r-- | pkg/setting/setting.go | 16 | ||||
-rw-r--r-- | public/less/_dashboard.less | 18 | ||||
-rw-r--r-- | routes/repo/setting.go | 58 | ||||
-rw-r--r-- | templates/explore/repo_list.tmpl | 11 | ||||
-rw-r--r-- | templates/org/team/repositories.tmpl | 1 | ||||
-rw-r--r-- | templates/repo/header.tmpl | 3 | ||||
-rw-r--r-- | templates/repo/settings/options.tmpl | 15 | ||||
-rw-r--r-- | templates/user/dashboard/dashboard.tmpl | 8 |
12 files changed, 191 insertions, 15 deletions
@@ -62,7 +62,7 @@ pkg/bindata/bindata.go: $(DATA_FILES) less: public/css/gogs.css public/css/gogs.css: $(LESS_FILES) - lessc $< $@ + lessc $< >$@ clean: go clean -i ./... @@ -100,6 +100,13 @@ func newMacaron() *macaron.Macaron { SkipLogging: setting.DisableRouterLog, }, )) + m.Use(macaron.Static( + setting.RepositoryAvatarUploadPath, + macaron.StaticOptions{ + Prefix: "repo-avatars", + SkipLogging: setting.DisableRouterLog, + }, + )) funcMap := template.NewFuncMap() m.Use(macaron.Renderer(macaron.RenderOptions{ @@ -419,6 +426,9 @@ func runWeb(c *cli.Context) error { m.Group("/settings", func() { m.Combo("").Get(repo.Settings). Post(bindIgnErr(form.RepoSetting{}), repo.SettingsPost) + m.Combo("/avatar").Get(repo.SettingsAvatar). + Post(binding.MultipartForm(form.Avatar{}), repo.SettingsAvatarPost) + m.Post("/avatar/delete", repo.SettingsDeleteAvatar) m.Group("/collaboration", func() { m.Combo("").Get(repo.SettingsCollaboration).Post(repo.SettingsCollaborationPost) m.Post("/access_mode", repo.ChangeCollaborationAccessMode) diff --git a/conf/app.ini b/conf/app.ini index 798712cb..b910d82b 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -286,6 +286,8 @@ CSRF_COOKIE_NAME = _csrf [picture] ; Path to store user uploaded avatars AVATAR_UPLOAD_PATH = data/avatars +; Path to store repository uploaded avatars +REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars ; Chinese users can choose "duoshuo" ; or a custom avatar source, like: http://cn.gravatar.com/avatar/ GRAVATAR_SOURCE = gravatar diff --git a/models/repo.go b/models/repo.go index b15b0175..3a9f388b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -15,10 +15,14 @@ import ( "sort" "strings" "time" + "image" + _ "image/jpeg" + "image/png" "github.com/Unknwon/cae/zip" "github.com/Unknwon/com" "github.com/go-xorm/xorm" + "github.com/nfnt/resize" "github.com/mcuadros/go-version" log "gopkg.in/clog.v1" "gopkg.in/ini.v1" @@ -27,6 +31,7 @@ import ( api "github.com/gogs/go-gogs-client" "github.com/gogs/gogs/models/errors" + "github.com/gogs/gogs/pkg/avatar" "github.com/gogs/gogs/pkg/bindata" "github.com/gogs/gogs/pkg/markup" "github.com/gogs/gogs/pkg/process" @@ -284,6 +289,61 @@ func (repo *Repository) HTMLURL() string { return setting.AppURL + repo.FullName() } +// CustomAvatarPath returns repository custom avatar file path. +func (repo *Repository) CustomAvatarPath() string { + return filepath.Join(setting.RepositoryAvatarUploadPath, com.ToStr(repo.ID)) +} + +// RelAvatarLink returns relative avatar link to the site domain, +// which includes app sub-url as prefix. +// Since Gravatar support not needed here - just check for image path. +func (repo *Repository) RelAvatarLink() string { + defaultImgUrl := "" + if !com.IsExist(repo.CustomAvatarPath()) { + return defaultImgUrl + } + return setting.AppSubURL + "/repo-avatars/" + com.ToStr(repo.ID) +} + +// AvatarLink returns user avatar absolute link. +func (repo *Repository) AvatarLink() string { + link := repo.RelAvatarLink() + if link[0] == '/' && link[1] != '/' { + return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:] + } + return link +} + +// UploadAvatar saves custom avatar for repository. +// FIXME: split uploads to different subdirs +// in case we have massive number of repositories. +func (repo *Repository) UploadAvatar(data []byte) error { + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("Decode: %v", err) + } + + m := resize.Resize(avatar.AVATAR_SIZE, avatar.AVATAR_SIZE, img, resize.NearestNeighbor) + os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm) + fw, err := os.Create(repo.CustomAvatarPath()) + if err != nil { + return fmt.Errorf("Create: %v", err) + } + defer fw.Close() + + if err = png.Encode(fw, m); err != nil { + return fmt.Errorf("Encode: %v", err) + } + + return nil +} + +// DeleteAvatar deletes the repository custom avatar. +func (repo *Repository) DeleteAvatar() error { + log.Trace("DeleteAvatar [%d]: %s", repo.ID, repo.CustomAvatarPath()) + return os.Remove(repo.CustomAvatarPath()) +} + // This method assumes following fields have been assigned with valid values: // Required - BaseRepo (if fork) // Arguments that are allowed to be nil: permission @@ -312,6 +372,8 @@ func (repo *Repository) APIFormat(permission *api.Permission, user ...*User) *ap Created: repo.Created, Updated: repo.Updated, Permissions: permission, +// Reserved for go-gogs-client change +// AvatarUrl: repo.AvatarLink(), } if repo.IsFork { p := &api.Permission{Pull: true} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b06c1bf3..5e239249 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -188,11 +188,12 @@ var ( } // Picture settings - AvatarUploadPath string - GravatarSource string - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar + AvatarUploadPath string + RepositoryAvatarUploadPath string + GravatarSource string + DisableGravatar bool + EnableFederatedAvatar bool + LibravatarService *libravatar.Libravatar // Log settings LogRootPath string @@ -611,6 +612,11 @@ func NewContext() { if !filepath.IsAbs(AvatarUploadPath) { AvatarUploadPath = path.Join(workDir, AvatarUploadPath) } + RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars")) + forcePathSeparator(RepositoryAvatarUploadPath) + if !filepath.IsAbs(RepositoryAvatarUploadPath) { + RepositoryAvatarUploadPath = path.Join(workDir, RepositoryAvatarUploadPath) + } switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": GravatarSource = "http://gravatar.duoshuo.com/avatar/" diff --git a/public/less/_dashboard.less b/public/less/_dashboard.less index 3c84b77d..688a023b 100644 --- a/public/less/_dashboard.less +++ b/public/less/_dashboard.less @@ -141,18 +141,28 @@ .repo-owner-name-list { .item-name { max-width: 70%; - margin-bottom: -4px; + margin-bottom: -4px; + } + .ui.micro.image { + width: 16px; + height: auto; + display: inline-block; } } #collaborative-repo-list { .owner-and-repo { - max-width: 80%; - margin-bottom: -5px; + max-width: 75%; + margin-bottom: -5px; } .owner-name { max-width: 120px; - margin-bottom: -5px; + margin-bottom: -5px; + } + .ui.micro.image { + width: 16px; + height: auto; + display: inline-block; } } } diff --git a/routes/repo/setting.go b/routes/repo/setting.go index 0b0f294d..da9d8fe4 100644 --- a/routes/repo/setting.go +++ b/routes/repo/setting.go @@ -8,9 +8,10 @@ import ( "fmt" "strings" "time" + "io/ioutil" log "gopkg.in/clog.v1" - + "github.com/Unknwon/com" "github.com/gogs/git-module" "github.com/gogs/gogs/models" @@ -19,10 +20,12 @@ import ( "github.com/gogs/gogs/pkg/form" "github.com/gogs/gogs/pkg/mailer" "github.com/gogs/gogs/pkg/setting" + "github.com/gogs/gogs/pkg/tool" ) const ( SETTINGS_OPTIONS = "repo/settings/options" + SETTINGS_REPO_AVATAR = "repo/settings/avatar" SETTINGS_COLLABORATION = "repo/settings/collaboration" SETTINGS_BRANCHES = "repo/settings/branches" SETTINGS_PROTECTED_BRANCH = "repo/settings/protected_branch" @@ -632,3 +635,56 @@ func DeleteDeployKey(c *context.Context) { "redirect": c.Repo.RepoLink + "/settings/keys", }) } + +func SettingsAvatar(c *context.Context) { + c.Title("settings.avatar") + c.PageIs("SettingsAvatar") + c.Success(SETTINGS_REPO_AVATAR) +} + +func SettingsAvatarPost(c *context.Context, f form.Avatar) { + f.Source = form.AVATAR_LOCAL + if err := UpdateAvatarSetting(c, f); err != nil { + c.Flash.Error(err.Error()) + } else { + c.Flash.Success(c.Tr("settings.update_avatar_success")) + } + c.SubURLRedirect(c.Repo.RepoLink + "/settings") +} + +func SettingsDeleteAvatar(c *context.Context) { + if err := c.Repo.Repository.DeleteAvatar(); err != nil { + c.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) + } + c.SubURLRedirect(c.Repo.RepoLink + "/settings") +} + +// FIXME: limit size. +func UpdateAvatarSetting(c *context.Context, f form.Avatar) error { + ctxRepo := c.Repo.Repository; + if f.Avatar != nil { + r, err := f.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %v", err) + } + defer r.Close() + + data, err := ioutil.ReadAll(r) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + if !tool.IsImageFile(data) { + return errors.New(c.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = ctxRepo.UploadAvatar(data); err != nil { + return fmt.Errorf("UploadAvatar: %v", err) + } + } else { + // No avatar is uploaded but setting has been changed to enable + // No random avatar here. + if !com.IsFile(ctxRepo.CustomAvatarPath()) { + log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID) + } + } + return nil +} diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index 43abad41..413cdc53 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -1,7 +1,12 @@ <div class="ui repository list"> {{range .Repos}} <div class="item"> - <div class="ui header"> + <div class="ui grid"> + <div class="ui two wide column middle aligned"> + {{if .RelAvatarLink}}<img class="ui tiny image" src="{{.RelAvatarLink}}">{{else}}<i class="mega-octicon octicon-repo"></i>{{end}} + </div> + <div class="ui fourteen wide column"> + <div class="ui header"> <a class="name" href="{{AppSubURL}}/{{if .Owner}}{{.Owner.Name}}{{else if $.Org}}{{$.Org.Name}}{{else}}{{$.Owner.Name}}{{end}}/{{.Name}}">{{if $.PageIsExplore}}{{.Owner.Name}} / {{end}}{{.Name}}</a> {{if .IsPrivate}} <span class="text gold"><i class="octicon octicon-lock"></i></span> @@ -9,6 +14,8 @@ <span><i class="octicon octicon-repo-forked"></i></span> {{else if .IsMirror}} <span><i class="octicon octicon-repo-clone"></i></span> + {{else}} + <span class="text"><i class="octicon octicon-globe"></i></span> {{end}} <div class="ui right metas"> @@ -18,6 +25,8 @@ </div> {{if .Description}}<p class="has-emoji">{{.Description | Str2html}}</p>{{end}} <p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSince .Updated $.i18n.Lang}}</p> + </div> + </div> </div> {{end}} </div> diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 4f1ad313..e2cfbe95 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -17,6 +17,7 @@ <a class="ui red small button right" href="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/remove?repoid={{.ID}}">{{$.i18n.Tr "org.teams.remove_repo"}}</a> {{end}} <a class="member" href="{{AppSubURL}}/{{$.Org.Name}}/{{.Name}}"> + <img height="16px" class="octicon" src="{{.RelAvatarLink}}" /> <i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i> <strong>{{$.Org.Name}}/{{.Name}}</strong> </a> diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 77685c5e..1bef1903 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -5,7 +5,8 @@ <div class="column"><!-- start column --> <div class="ui header"> <div class="ui huge breadcrumb"> - <i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}repo{{end}}"></i> + {{if .RelAvatarLink}}<img class="ui mini spaced image" src="{{.RelAvatarLink}}">{{else}}<i class="mega-octicon octicon-repo"></i>{{end}} + <i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}globe{{end}}"></i> <a href="{{AppSubURL}}/{{.Owner.Name}}">{{.Owner.Name}}</a> <div class="divider"> / </div> <a href="{{$.RepoLink}}">{{.Name}}</a> diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 83966254..8aa4a1f7 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -41,6 +41,21 @@ <div class="field"> <button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button> </div> + + </form> + + <div class="ui divider"></div> + + <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data"> + {{.CSRFTokenHTML}} + <div class="inline field"> + <label for="avatar">{{.i18n.Tr "settings.choose_new_avatar"}}</label> + <input name="avatar" type="file" > + </div> + <div class="field"> + <button class="ui green button">{{$.i18n.Tr "settings.update_avatar"}}</button> + <a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.i18n.Tr "settings.delete_current_avatar"}}</a> + </div> </form> </div> diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl index 4a0c90a2..f9f97447 100644 --- a/templates/user/dashboard/dashboard.tmpl +++ b/templates/user/dashboard/dashboard.tmpl @@ -32,7 +32,8 @@ {{range .Repos}} <li {{if .IsPrivate}}class="private"{{end}}> <a href="{{AppSubURL}}/{{$.ContextUser.Name}}/{{.Name}}"> - <i class="octicon octicon-{{if .IsFork}}repo-forked{{else if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i> + {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}} + <i class="octicon octicon-{{if .IsFork}}repo-forked{{else if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else}}globe{{end}}"></i> <strong class="text truncate item-name">{{.Name}}</strong> <span class="ui right text light grey"> {{.NumStars}} <i class="octicon octicon-star rear"></i> @@ -57,7 +58,8 @@ {{range .CollaborativeRepos}} <li {{if .IsPrivate}}class="private"{{end}}> <a href="{{AppSubURL}}/{{.Owner.Name}}/{{.Name}}"> - <i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i> + {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}} + <i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}globe{{end}}"></i> <span class="text truncate owner-and-repo"> <span class="text truncate owner-name">{{.Owner.Name}}</span> / <strong>{{.Name}}</strong> </span> @@ -88,6 +90,7 @@ {{range .ContextUser.Orgs}} <li> <a href="{{AppSubURL}}/{{.Name}}"> + {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}} <i class="octicon octicon-organization"></i> <strong class="text truncate item-name">{{.Name}}</strong> <span class="ui right text light grey"> @@ -116,6 +119,7 @@ {{range .Mirrors}} <li {{if .IsPrivate}}class="private"{{end}}> <a href="{{AppSubURL}}/{{$.ContextUser.Name}}/{{.Name}}"> + {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}} <i class="octicon octicon-repo-clone"></i> <strong class="text truncate item-name">{{.Name}}</strong> <span class="ui right text light grey"> |