diff --git a/cmd/web.go b/cmd/web.go index 9a709e1cd..b67adb37d 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -230,6 +230,7 @@ func runWeb(c *cli.Context) error { m.Group("/repositories", func() { m.Get("", user.SettingsRepos) m.Post("/leave", user.SettingsLeaveRepo) + m.Post("/labels", user.SettingsAddOrEditRepoLabel) }) m.Group("/organizations", func() { m.Get("", user.SettingsOrganizations) diff --git a/models/models.go b/models/models.go index 6fcd26194..3b383f158 100644 --- a/models/models.go +++ b/models/models.go @@ -65,7 +65,7 @@ func init() { new(Mirror), new(Release), new(LoginSource), new(Webhook), new(HookTask), new(ProtectBranch), new(ProtectBranchWhitelist), new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), - new(Notice), new(EmailAddress)) + new(Notice), new(EmailAddress), new (RepositoryLabel)) gonicNames := []string{"SSL"} for _, name := range gonicNames { diff --git a/models/repo.go b/models/repo.go index 6ed7cd183..acbb33e8b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -8,6 +8,8 @@ import ( "bytes" "fmt" "io/ioutil" + "html/template" + "strconv" "os" "os/exec" "path" @@ -139,6 +141,23 @@ func NewRepoContext() { RemoveAllWithNotice("Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp")) } +// Repository label +type RepositoryLabel struct { + ID int64 + OwnerID int64 + Owner *User `xorm:"-"` + Name string `xorm:"INDEX NOT NULL"` + Color string `xorm:"VARCHAR(7)"` + IsPrivate bool + + NumRepo int + + Created time.Time `xorm:"-"` + CreatedUnix int64 + Updated time.Time `xorm:"-"` + UpdatedUnix int64 +} + // Repository contains information of a repository. type Repository struct { ID int64 @@ -1619,6 +1638,128 @@ func GetUserAndCollaborativeRepositories(userID int64) ([]*Repository, error) { return append(repos, ownRepos...), nil } +// ForegroundColor calculates the text color for labels based +// on their background color. +func (l *RepositoryLabel) ForegroundColor() template.CSS { + if strings.HasPrefix(l.Color, "#") { + if color, err := strconv.ParseUint(l.Color[1:], 16, 64); err == nil { + r := float32(0xFF & (color >> 16)) + g := float32(0xFF & (color >> 8)) + b := float32(0xFF & color) + luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255 + + if luminance < 0.66 { + return template.CSS("#fff") + } + } + } + + // default to black + return template.CSS("#000") +} + + +func CreateRepositoryLabel(owner *User, opts *CreateRepoLabelOptions) (_ *RepositoryLabel, err error) { + if !owner.CanCreateRepo() { + return nil, errors.ReachLimitOfRepo{owner.RepoCreationNum()} + } + // FIXME check color is /#[0-9A-Fa-F]{6}/ + + repoLabel := &RepositoryLabel{ + OwnerID: owner.ID, + Owner: owner, + Name: opts.Name, + Color: opts.Color, + IsPrivate: opts.IsPrivate, + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return nil, err + } + + if err = createRepositoryLabel(sess, repoLabel); err != nil { + return nil, err + } + + return repoLabel, sess.Commit() +} + +func UpdateRepositoryLabel(id int64, owner *User, opts *CreateRepoLabelOptions) (_ *RepositoryLabel, err error) { + if !owner.CanCreateRepo() { + return nil, errors.ReachLimitOfRepo{owner.RepoCreationNum()} + } + // FIXME check color is /#[0-9A-Fa-F]{6}/ + + repoLabel, err := GetRepositoryLabel(id) + if err != nil { + return nil, err + } + + // FIXME check user.ID is repoLabel.OwnerID + repoLabel.Name = opts.Name + repoLabel.Color = opts.Color + repoLabel.IsPrivate = opts.IsPrivate + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return nil, err + } + + // Use same rules for label name than for repository names + if err = IsUsableRepoName(repoLabel.Name); err != nil { + return nil, err + } + + if _, err = sess.ID(repoLabel.ID).Update(repoLabel); err != nil { + return nil, err + } + + return repoLabel, sess.Commit() +} + +func GetRepositoryLabels(userID int64) ([]*RepositoryLabel, error) { + labels := make([]*RepositoryLabel, 0, 5) + if err := x.Where("owner_id = ?", userID).Find(&labels); err != nil { + return nil, fmt.Errorf("select repository labels: %v", err) + } + + return labels, nil +} + +func GetRepositoryLabel(labelId int64) (*RepositoryLabel, error) { + label := new(RepositoryLabel) + + if has, err := x.Where("ID = ?", labelId).Get(label); err != nil { + return nil, fmt.Errorf("select repository labels: %v", err) + } else if !has { + return nil, fmt.Errorf("Label %d not found", labelId) + } + + return label, nil +} + +type CreateRepoLabelOptions struct { + Name string + IsPrivate bool + Color string +} + +func createRepositoryLabel(e *xorm.Session, repoLabel *RepositoryLabel) (err error) { + // Use same rules for label name than for repository names + if err = IsUsableRepoName(repoLabel.Name); err != nil { + return err + } + + if _, err = e.Insert(repoLabel); err != nil { + return err + } + + return nil +} + func getRepositoryCount(e Engine, u *User) (int64, error) { return x.Count(&Repository{OwnerID: u.ID}) } diff --git a/public/css/gogs.css b/public/css/gogs.css index b064c1e7e..5bea00c54 100644 --- a/public/css/gogs.css +++ b/public/css/gogs.css @@ -2613,25 +2613,30 @@ footer .ui.language .menu { background: #f0f0f0; } .edit-label.modal .form .column, +.add-edit-label.modal .form .column, .new-label.segment .form .column { padding-right: 0; } .edit-label.modal .form .buttons, +.add-edit-label.modal .form .buttons, .new-label.segment .form .buttons { margin-left: auto; padding-top: 15px; } .edit-label.modal .form .color.picker.column, +.add-edit-label.modal .form .color.picker.column, .new-label.segment .form .color.picker.column { width: auto; } .edit-label.modal .form .color.picker.column .color-picker, +.add-edit-label.modal .form .color.picker.column .color-picker, .new-label.segment .form .color.picker.column .color-picker { height: 35px; width: auto; padding-left: 30px; } .edit-label.modal .form .minicolors-swatch.minicolors-sprite, +.add-edit-label.modal .form .minicolors-swatch.minicolors-sprite, .new-label.segment .form .minicolors-swatch.minicolors-sprite { top: 10px; left: 10px; @@ -2639,6 +2644,7 @@ footer .ui.language .menu { height: 15px; } .edit-label.modal .form .precolors, +.add-edit-label.modal .form .precolors, .new-label.segment .form .precolors { padding-left: 0; padding-right: 0; @@ -2646,6 +2652,7 @@ footer .ui.language .menu { width: 120px; } .edit-label.modal .form .precolors .color, +.add-edit-label.modal .form .precolors .color, .new-label.segment .form .precolors .color { float: left; width: 15px; diff --git a/public/js/gogs.js b/public/js/gogs.js index 4eefff5ce..a7d643f3a 100644 --- a/public/js/gogs.js +++ b/public/js/gogs.js @@ -1089,6 +1089,51 @@ function initUserSettings() { } }); } + + // Repository labels + if ($('.repo-labels').length > 0) { + var $newLabelPanel = $('.add-edit-label.modal'), + $header = $newLabelPanel.find('.header'), + $submitButton = $newLabelPanel.find('.actions .positive'); + + $newLabelPanel.find('.color-picker').first().minicolors(); + + $('.new-label.button').click(function () { + $('.label-name').val(''); + $('.label-id').val(''); + $header.html($header.data('createtext')); + $submitButton.html($submitButton.data('createtext')); + + var defaultColor = $newLabelPanel.find('.precolors .color').first().data('color-hex'); + $('.color-picker').val(defaultColor); + $('.minicolors-swatch-color').css("background-color", defaultColor); + + $newLabelPanel.modal('show'); + }); + $('.edit-label-button').click(function() { + var $this = $(this); + $('.label-name').val($this.data('name')); + $('.label-id').val($this.data('id')); + $header.html($header.data('edittext')); + $submitButton.html($submitButton.data('edittext')); + + $('.color-picker').val($this.data('color')); + $('.minicolors-swatch-color').css("background-color", $this.data('color')); + + $newLabelPanel.modal('show'); + }); + $('.add-edit-label .submit').click(function (e) { + e.stopPropagation(); + $('.add-edit-label .form').submit(); + }); + + $('.precolors .color').click(function () { + var color = $(this).data('color-hex'); + $('.color-picker').val(color); + $('.minicolors-swatch-color').css("background-color", color); + }); + } + } function initRepositoryCollaboration() { diff --git a/public/less/_repository.less b/public/less/_repository.less index 04053c8b9..14f5c7bb1 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -1643,6 +1643,7 @@ } .edit-label.modal, +.add-edit-label.modal, .new-label.segment { .form { .column { diff --git a/routes/user/setting.go b/routes/user/setting.go index 723b3da24..39349fbef 100644 --- a/routes/user/setting.go +++ b/routes/user/setting.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/base64" "fmt" + "strconv" "html/template" "image/png" "io/ioutil" @@ -521,10 +522,46 @@ func SettingsRepos(c *context.Context) { return } c.Data["Repos"] = repos + c.Data["RequireMinicolors"] = true + + repoLabels, err := models.GetRepositoryLabels(c.User.ID) + if err != nil { + c.ServerError("GetUserAndCollaborativeRepositories", err) + return + } + if err = models.RepositoryList(repos).LoadAttributes(); err != nil { + c.ServerError("LoadAttributes", err) + return + } + c.Data["RepoLabels"] = repoLabels c.Success(SETTINGS_REPOSITORIES) } +func SettingsAddOrEditRepoLabel(c *context.Context) { + id, _ := strconv.ParseInt(c.Query("id"), 10, 64) + repoLabelOptions := &models.CreateRepoLabelOptions{ + Name: c.Query("name"), + Color: c.Query("color"), + IsPrivate: false, + } + if id != 0 { + // update + if _, err := models.UpdateRepositoryLabel(id, c.User, repoLabelOptions); err != nil { + c.ServerError("UpdateRepositoryLabel", err) + return + } + c.Redirect(setting.AppSubURL + "/user/settings/repositories") + } else { + // create + if _, err := models.CreateRepositoryLabel(c.User, repoLabelOptions); err != nil { + c.ServerError("AddRepositoryLabel", err) + return + } + c.Redirect(setting.AppSubURL + "/user/settings/repositories") + } +} + func SettingsLeaveRepo(c *context.Context) { repo, err := models.GetRepositoryByID(c.QueryInt64("id")) if err != nil { diff --git a/templates/user/settings/repositories.tmpl b/templates/user/settings/repositories.tmpl index 6500d1494..9e025cea0 100644 --- a/templates/user/settings/repositories.tmpl +++ b/templates/user/settings/repositories.tmpl @@ -37,7 +37,64 @@ {{end}} + +

+ {{.i18n.Tr "settings.repoLabels"}} +
+
{{.i18n.Tr "settings.add_repo_label"}}
+
+

+ +
+
+ {{range .RepoLabels}} + + {{end}} +
+
+ + + + + +