forked from kevadesu/forgejo
Merge pull request 'feat: Add summary card for repos and releases' (#6269) from JakobDev/forgejo:repocard into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6269 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Reviewed-by: Otto <otto@codeberg.org>
This commit is contained in:
commit
b01f3b9b05
13 changed files with 735 additions and 282 deletions
526
routers/web/repo/card.go
Normal file
526
routers/web/repo/card.go
Normal file
|
@ -0,0 +1,526 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/card"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// drawUser draws a user avatar in a summary card
|
||||
func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) error {
|
||||
if user.UseCustomAvatar {
|
||||
posterAvatarPath := user.CustomAvatarRelativePath()
|
||||
if posterAvatarPath != "" {
|
||||
userAvatarFile, err := storage.Avatars.Open(user.CustomAvatarRelativePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userAvatarImage, _, err := image.Decode(userAvatarFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
card.DrawImage(userAvatarImage)
|
||||
}
|
||||
} else {
|
||||
posterAvatarLink := user.AvatarLinkWithSize(ctx, 256)
|
||||
card.DrawExternalImage(posterAvatarLink)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawRepoIcon draws the repo icon in a summary card
|
||||
func drawRepoIcon(ctx *context.Context, card *card.Card, repo *repo_model.Repository) error {
|
||||
repoAvatarPath := repo.CustomAvatarRelativePath()
|
||||
|
||||
if repoAvatarPath != "" {
|
||||
repoAvatarFile, err := storage.RepoAvatars.Open(repoAvatarPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repoAvatarImage, _, err := image.Decode(repoAvatarFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
card.DrawImage(repoAvatarImage)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon
|
||||
err := repo.LoadOwner(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if repo.Owner != nil {
|
||||
err = drawUser(ctx, card, repo.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hexToColor converts a hex color to a go color
|
||||
func hexToColor(colorStr string) (*color.RGBA, error) {
|
||||
colorStr = strings.TrimLeft(colorStr, "#")
|
||||
|
||||
b, err := hex.DecodeString(colorStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(b) < 3 {
|
||||
return nil, fmt.Errorf("expected at least 3 bytes from DecodeString, got %d", len(b))
|
||||
}
|
||||
|
||||
color := color.RGBA{b[0], b[1], b[2], 255}
|
||||
|
||||
return &color, nil
|
||||
}
|
||||
|
||||
func drawLanguagesCard(ctx *context.Context, card *card.Card) error {
|
||||
languageList, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(languageList) == 0 {
|
||||
card.DrawRect(0, 0, card.Width, card.Height, color.White)
|
||||
return nil
|
||||
}
|
||||
|
||||
currentX := 0
|
||||
var langColor *color.RGBA
|
||||
|
||||
for _, lang := range languageList {
|
||||
langColor, err = hexToColor(lang.Color)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
langWidth := float32(card.Width) * (lang.Percentage / 100)
|
||||
card.DrawRect(currentX, 0, currentX+int(langWidth), card.Width, langColor)
|
||||
currentX += int(langWidth)
|
||||
}
|
||||
|
||||
if currentX < card.Width {
|
||||
card.DrawRect(currentX, 0, card.Width, card.Height, langColor)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func drawRepoSummaryCard(ctx *context.Context, repo *repo_model.Repository) (*card.Card, error) {
|
||||
width, height := card.DefaultSize()
|
||||
mainCard, err := card.NewCard(width, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentCard, languageBarCard := mainCard.Split(false, 90)
|
||||
|
||||
contentCard.SetMargin(60)
|
||||
topSection, bottomSection := contentCard.Split(false, 75)
|
||||
issueSummary, issueIcon := topSection.Split(true, 80)
|
||||
repoInfo, issueDescription := issueSummary.Split(false, 30)
|
||||
|
||||
repoInfo.SetMargin(10)
|
||||
_, err = repoInfo.DrawText(repo.FullName(), color.Black, 56, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueDescription.SetMargin(10)
|
||||
_, err = issueDescription.DrawText(repo.Description, color.Gray{128}, 36, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueIcon.SetMargin(10)
|
||||
err = drawRepoIcon(ctx, issueIcon, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topCountCard, bottomCountCard := bottomSection.Split(false, 50)
|
||||
|
||||
releaseCount, err := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||
IncludeDrafts: ctx.Repo.CanWrite(unit_model.TypeReleases),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
starsText := ctx.Locale.TrN(
|
||||
repo.NumStars,
|
||||
"explore.stars_one",
|
||||
"explore.stars_few",
|
||||
repo.NumStars,
|
||||
)
|
||||
forksText := ctx.Locale.TrN(
|
||||
repo.NumForks,
|
||||
"explore.forks_one",
|
||||
"explore.forks_few",
|
||||
repo.NumForks,
|
||||
)
|
||||
releasesText := ctx.Locale.TrN(
|
||||
releaseCount,
|
||||
"repo.activity.title.releases_1",
|
||||
"repo.activity.title.releases_n",
|
||||
releaseCount,
|
||||
)
|
||||
|
||||
topCountText := fmt.Sprintf("%s • %s • %s", starsText, forksText, releasesText)
|
||||
|
||||
topCountCard.SetMargin(10)
|
||||
_, err = topCountCard.DrawText(topCountText, color.Gray{128}, 36, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issuesText := ctx.Locale.TrN(
|
||||
repo.NumOpenIssues,
|
||||
"repo.activity.title.issues_1",
|
||||
"repo.activity.title.issues_n",
|
||||
repo.NumOpenIssues,
|
||||
)
|
||||
pullRequestsText := ctx.Locale.TrN(
|
||||
repo.NumOpenPulls,
|
||||
"repo.activity.title.prs_1",
|
||||
"repo.activity.title.prs_n",
|
||||
repo.NumOpenPulls,
|
||||
)
|
||||
|
||||
bottomCountText := fmt.Sprintf("%s • %s", issuesText, pullRequestsText)
|
||||
|
||||
bottomCountCard.SetMargin(10)
|
||||
_, err = bottomCountCard.DrawText(bottomCountText, color.Gray{128}, 36, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = drawLanguagesCard(ctx, languageBarCard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mainCard, nil
|
||||
}
|
||||
|
||||
func drawIssueSummaryCard(ctx *context.Context, issue *issue_model.Issue) (*card.Card, error) {
|
||||
width, height := card.DefaultSize()
|
||||
mainCard, err := card.NewCard(width, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mainCard.SetMargin(60)
|
||||
topSection, bottomSection := mainCard.Split(false, 75)
|
||||
issueSummary, issueIcon := topSection.Split(true, 80)
|
||||
repoInfo, issueDescription := issueSummary.Split(false, 15)
|
||||
|
||||
repoInfo.SetMargin(10)
|
||||
_, err = repoInfo.DrawText(fmt.Sprintf("%s - #%d", issue.Repo.FullName(), issue.Index), color.Gray{128}, 36, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueDescription.SetMargin(10)
|
||||
_, err = issueDescription.DrawText(issue.Title, color.Black, 56, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueIcon.SetMargin(10)
|
||||
err = drawRepoIcon(ctx, issueIcon, issue.Repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueStats, issueAttribution := bottomSection.Split(false, 50)
|
||||
|
||||
var state string
|
||||
if issue.IsPull && issue.PullRequest.HasMerged {
|
||||
if issue.PullRequest.Status == 3 {
|
||||
state = ctx.Locale.TrString("repo.pulls.manually_merged")
|
||||
} else {
|
||||
state = ctx.Locale.TrString("repo.pulls.merged")
|
||||
}
|
||||
} else if issue.IsClosed {
|
||||
state = ctx.Locale.TrString("repo.issues.closed_title")
|
||||
} else if issue.IsPull {
|
||||
if issue.PullRequest.IsWorkInProgress(ctx) {
|
||||
state = ctx.Locale.TrString("repo.issues.draft_title")
|
||||
} else {
|
||||
state = ctx.Locale.TrString("repo.issues.open_title")
|
||||
}
|
||||
} else {
|
||||
state = ctx.Locale.TrString("repo.issues.open_title")
|
||||
}
|
||||
state = strings.ToLower(state)
|
||||
|
||||
issueStats.SetMargin(10)
|
||||
if issue.IsPull {
|
||||
reviews := map[int64]bool{}
|
||||
for _, comment := range issue.Comments {
|
||||
if comment.Review != nil {
|
||||
reviews[comment.Review.ID] = true
|
||||
}
|
||||
}
|
||||
_, err = issueStats.DrawText(
|
||||
fmt.Sprintf("%s, %s, %s",
|
||||
ctx.Locale.TrN(
|
||||
issue.NumComments,
|
||||
"repo.issues.num_comments_1",
|
||||
"repo.issues.num_comments",
|
||||
issue.NumComments,
|
||||
),
|
||||
ctx.Locale.TrN(
|
||||
len(reviews),
|
||||
"repo.issues.num_reviews_one",
|
||||
"repo.issues.num_reviews_few",
|
||||
len(reviews),
|
||||
),
|
||||
state,
|
||||
),
|
||||
color.Gray{128}, 36, card.Top, card.Left)
|
||||
} else {
|
||||
_, err = issueStats.DrawText(
|
||||
fmt.Sprintf("%s, %s",
|
||||
ctx.Locale.TrN(
|
||||
issue.NumComments,
|
||||
"repo.issues.num_comments_1",
|
||||
"repo.issues.num_comments",
|
||||
issue.NumComments,
|
||||
),
|
||||
state,
|
||||
),
|
||||
color.Gray{128}, 36, card.Top, card.Left)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueAttributionIcon, issueAttributionText := issueAttribution.Split(true, 8)
|
||||
issueAttributionText.SetMargin(5)
|
||||
_, err = issueAttributionText.DrawText(
|
||||
fmt.Sprintf(
|
||||
"%s - %s",
|
||||
issue.Poster.Name,
|
||||
issue.Created.AsTime().Format(time.DateOnly),
|
||||
),
|
||||
color.Gray{128}, 36, card.Middle, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = drawUser(ctx, issueAttributionIcon, issue.Poster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mainCard, nil
|
||||
}
|
||||
|
||||
func drawReleaseSummaryCard(ctx *context.Context, release *repo_model.Release) (*card.Card, error) {
|
||||
width, height := card.DefaultSize()
|
||||
mainCard, err := card.NewCard(width, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mainCard.SetMargin(60)
|
||||
topSection, bottomSection := mainCard.Split(false, 75)
|
||||
releaseSummary, repoIcon := topSection.Split(true, 80)
|
||||
repoInfo, releaseDescription := releaseSummary.Split(false, 15)
|
||||
|
||||
repoInfo.SetMargin(10)
|
||||
_, err = repoInfo.DrawText(release.Repo.FullName(), color.Gray{128}, 36, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
releaseDescription.SetMargin(10)
|
||||
_, err = releaseDescription.DrawText(release.DisplayName(), color.Black, 56, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoIcon.SetMargin(10)
|
||||
err = drawRepoIcon(ctx, repoIcon, release.Repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadCountCard, releaseDateCard := bottomSection.Split(true, 75)
|
||||
|
||||
downloadCount, err := release.GetTotalDownloadCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadCountText := ctx.Locale.TrN(
|
||||
strconv.FormatInt(downloadCount, 10),
|
||||
"repo.release.download_count_one",
|
||||
"repo.release.download_count_few",
|
||||
strconv.FormatInt(downloadCount, 10),
|
||||
)
|
||||
|
||||
_, err = downloadCountCard.DrawText(string(downloadCountText), color.Gray{128}, 36, card.Bottom, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = releaseDateCard.DrawText(release.CreatedUnix.AsTime().Format(time.DateOnly), color.Gray{128}, 36, card.Bottom, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mainCard, nil
|
||||
}
|
||||
|
||||
// checkCardCache checks if a card in cache and serves it
|
||||
func checkCardCache(ctx *context.Context, cacheKey string) bool {
|
||||
cache := cache.GetCache()
|
||||
pngData, ok := cache.Get(cacheKey).([]byte)
|
||||
if ok && pngData != nil && len(pngData) > 0 {
|
||||
ctx.Resp.Header().Set("Content-Type", "image/png")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, err := ctx.Resp.Write(pngData)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSummaryCard", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// serveCard server a Card to the user adds it to the cache
|
||||
func serveCard(ctx *context.Context, card *card.Card, cacheKey string) {
|
||||
cache := cache.GetCache()
|
||||
|
||||
// Encode image, store in cache
|
||||
var imageBuffer bytes.Buffer
|
||||
err := png.Encode(&imageBuffer, card.Img)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSummaryCard", err)
|
||||
return
|
||||
}
|
||||
imageBytes := imageBuffer.Bytes()
|
||||
err = cache.Put(cacheKey, imageBytes, setting.CacheService.TTLSeconds())
|
||||
if err != nil {
|
||||
// don't abort serving the image if we just had a cache storage failure
|
||||
log.Warn("failed to cache issue summary card: %v", err)
|
||||
}
|
||||
|
||||
// Finish the uncached image response
|
||||
ctx.Resp.Header().Set("Content-Type", "image/png")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, err = ctx.Resp.Write(imageBytes)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSummaryCard", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func DrawRepoSummaryCard(ctx *context.Context) {
|
||||
cacheKey := fmt.Sprintf("summary_card:repo:%s:%d", ctx.Locale.Language(), ctx.Repo.Repository.ID)
|
||||
|
||||
if checkCardCache(ctx, cacheKey) {
|
||||
return
|
||||
}
|
||||
|
||||
card, err := drawRepoSummaryCard(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("drawRepoSummaryCar", err)
|
||||
return
|
||||
}
|
||||
|
||||
serveCard(ctx, card, cacheKey)
|
||||
}
|
||||
|
||||
func DrawIssueSummaryCard(ctx *context.Context) {
|
||||
issue, err := issue_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
if issue_model.IsErrIssueNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("summary_card:issue:%s:%d", ctx.Locale.Language(), issue.ID)
|
||||
|
||||
if checkCardCache(ctx, cacheKey) {
|
||||
return
|
||||
}
|
||||
|
||||
card, err := drawIssueSummaryCard(ctx, issue)
|
||||
if err != nil {
|
||||
ctx.ServerError("drawIssueSummaryCar", err)
|
||||
return
|
||||
}
|
||||
|
||||
serveCard(ctx, card, cacheKey)
|
||||
}
|
||||
|
||||
func DrawReleaseSummaryCard(ctx *context.Context) {
|
||||
release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*"))
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetReleaseForRepoByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = release.LoadRepo(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadRepo", err)
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("summary_card:release:%s:%d", ctx.Locale.Language(), release.ID)
|
||||
|
||||
if checkCardCache(ctx, cacheKey) {
|
||||
return
|
||||
}
|
||||
|
||||
card, err := drawReleaseSummaryCard(ctx, release)
|
||||
if err != nil {
|
||||
ctx.ServerError("drawRepoSummaryCar", err)
|
||||
return
|
||||
}
|
||||
|
||||
serveCard(ctx, card, cacheKey)
|
||||
}
|
|
@ -10,9 +10,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -34,8 +31,6 @@ import (
|
|||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/card"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
@ -47,7 +42,6 @@ import (
|
|||
"code.gitea.io/gitea/modules/optional"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/templates/vars"
|
||||
|
@ -2076,6 +2070,8 @@ func ViewIssue(ctx *context.Context) {
|
|||
ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName()
|
||||
ctx.Data["NewPinAllowed"] = pinAllowed
|
||||
ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
|
||||
ctx.Data["OpenGraphImageURL"] = issue.SummaryCardURL()
|
||||
ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.issues.summary_card_alt", issue.Title, issue.Repo.FullName())
|
||||
|
||||
prepareHiddenCommentType(ctx)
|
||||
if ctx.Written() {
|
||||
|
@ -2233,222 +2229,6 @@ func GetIssueInfo(ctx *context.Context) {
|
|||
ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue))
|
||||
}
|
||||
|
||||
// GetSummaryCard get an issue of a repository
|
||||
func GetSummaryCard(ctx *context.Context) {
|
||||
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cache := cache.GetCache()
|
||||
cacheKey := fmt.Sprintf("summary_card:issue:%s:%d", ctx.Locale.Language(), issue.ID)
|
||||
pngData, ok := cache.Get(cacheKey).([]byte)
|
||||
if ok && pngData != nil && len(pngData) > 0 {
|
||||
ctx.Resp.Header().Set("Content-Type", "image/png")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, err = ctx.Resp.Write(pngData)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSummaryCard", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
card, err := drawSummaryCard(ctx, issue)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSummaryCard", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode image, store in cache
|
||||
var imageBuffer bytes.Buffer
|
||||
err = png.Encode(&imageBuffer, card.Img)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSummaryCard", err)
|
||||
return
|
||||
}
|
||||
imageBytes := imageBuffer.Bytes()
|
||||
err = cache.Put(cacheKey, imageBytes, setting.CacheService.TTLSeconds())
|
||||
if err != nil {
|
||||
// don't abort serving the image if we just had a cache storage failure
|
||||
log.Warn("failed to cache issue summary card: %v", err)
|
||||
}
|
||||
|
||||
// Finish the uncached image response
|
||||
ctx.Resp.Header().Set("Content-Type", "image/png")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, err = ctx.Resp.Write(imageBytes)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSummaryCard", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func drawSummaryCard(ctx *context.Context, issue *issues_model.Issue) (*card.Card, error) {
|
||||
width, height := issue.SummaryCardSize()
|
||||
mainCard, err := card.NewCard(width, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mainCard.SetMargin(60)
|
||||
topSection, bottomSection := mainCard.Split(false, 75)
|
||||
issueSummary, issueIcon := topSection.Split(true, 80)
|
||||
repoInfo, issueDescription := issueSummary.Split(false, 15)
|
||||
|
||||
repoInfo.SetMargin(10)
|
||||
_, err = repoInfo.DrawText(fmt.Sprintf("%s - #%d", issue.Repo.FullName(), issue.Index), color.Gray{128}, 36, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueDescription.SetMargin(10)
|
||||
_, err = issueDescription.DrawText(issue.Title, color.Black, 56, card.Top, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueIcon.SetMargin(10)
|
||||
|
||||
repoAvatarPath := issue.Repo.CustomAvatarRelativePath()
|
||||
if repoAvatarPath != "" {
|
||||
repoAvatarFile, err := storage.RepoAvatars.Open(repoAvatarPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoAvatarImage, _, err := image.Decode(repoAvatarFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issueIcon.DrawImage(repoAvatarImage)
|
||||
} else {
|
||||
// If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon
|
||||
err = issue.Repo.LoadOwner(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if issue.Repo.Owner != nil {
|
||||
err = drawUser(ctx, issueIcon, issue.Repo.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
issueStats, issueAttribution := bottomSection.Split(false, 50)
|
||||
|
||||
var state string
|
||||
if issue.IsPull && issue.PullRequest.HasMerged {
|
||||
if issue.PullRequest.Status == 3 {
|
||||
state = ctx.Locale.TrString("repo.pulls.manually_merged")
|
||||
} else {
|
||||
state = ctx.Locale.TrString("repo.pulls.merged")
|
||||
}
|
||||
} else if issue.IsClosed {
|
||||
state = ctx.Locale.TrString("repo.issues.closed_title")
|
||||
} else if issue.IsPull {
|
||||
if issue.PullRequest.IsWorkInProgress(ctx) {
|
||||
state = ctx.Locale.TrString("repo.issues.draft_title")
|
||||
} else {
|
||||
state = ctx.Locale.TrString("repo.issues.open_title")
|
||||
}
|
||||
} else {
|
||||
state = ctx.Locale.TrString("repo.issues.open_title")
|
||||
}
|
||||
state = strings.ToLower(state)
|
||||
|
||||
issueStats.SetMargin(10)
|
||||
if issue.IsPull {
|
||||
reviews := map[int64]bool{}
|
||||
for _, comment := range issue.Comments {
|
||||
if comment.Review != nil {
|
||||
reviews[comment.Review.ID] = true
|
||||
}
|
||||
}
|
||||
_, err = issueStats.DrawText(
|
||||
fmt.Sprintf("%s, %s, %s",
|
||||
ctx.Locale.TrN(
|
||||
issue.NumComments,
|
||||
"repo.issues.num_comments_1",
|
||||
"repo.issues.num_comments",
|
||||
issue.NumComments,
|
||||
),
|
||||
ctx.Locale.TrN(
|
||||
len(reviews),
|
||||
"repo.issues.num_reviews_one",
|
||||
"repo.issues.num_reviews_few",
|
||||
len(reviews),
|
||||
),
|
||||
state,
|
||||
),
|
||||
color.Gray{128}, 36, card.Top, card.Left)
|
||||
} else {
|
||||
_, err = issueStats.DrawText(
|
||||
fmt.Sprintf("%s, %s",
|
||||
ctx.Locale.TrN(
|
||||
issue.NumComments,
|
||||
"repo.issues.num_comments_1",
|
||||
"repo.issues.num_comments",
|
||||
issue.NumComments,
|
||||
),
|
||||
state,
|
||||
),
|
||||
color.Gray{128}, 36, card.Top, card.Left)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issueAttributionIcon, issueAttributionText := issueAttribution.Split(true, 8)
|
||||
issueAttributionText.SetMargin(5)
|
||||
_, err = issueAttributionText.DrawText(
|
||||
fmt.Sprintf(
|
||||
"%s - %s",
|
||||
issue.Poster.Name,
|
||||
issue.Created.AsTime().Format("2006-01-02"),
|
||||
),
|
||||
color.Gray{128}, 36, card.Middle, card.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = drawUser(ctx, issueAttributionIcon, issue.Poster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mainCard, nil
|
||||
}
|
||||
|
||||
func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) error {
|
||||
if user.UseCustomAvatar {
|
||||
posterAvatarPath := user.CustomAvatarRelativePath()
|
||||
if posterAvatarPath != "" {
|
||||
userAvatarFile, err := storage.Avatars.Open(user.CustomAvatarRelativePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userAvatarImage, _, err := image.Decode(userAvatarFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
card.DrawImage(userAvatarImage)
|
||||
}
|
||||
} else {
|
||||
posterAvatarLink := user.AvatarLinkWithSize(ctx, 256)
|
||||
card.DrawExternalImage(posterAvatarLink)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateIssueTitle change issue's title
|
||||
func UpdateIssueTitle(ctx *context.Context) {
|
||||
issue := GetActionIssue(ctx)
|
||||
|
|
|
@ -365,11 +365,7 @@ func SingleRelease(ctx *context.Context) {
|
|||
addVerifyTagToContext(ctx)
|
||||
|
||||
ctx.Data["PageIsSingleTag"] = release.IsTag
|
||||
if release.IsTag {
|
||||
ctx.Data["Title"] = release.TagName
|
||||
} else {
|
||||
ctx.Data["Title"] = release.Title
|
||||
}
|
||||
ctx.Data["Title"] = release.DisplayName()
|
||||
|
||||
err = release.LoadArchiveDownloadCount(ctx)
|
||||
if err != nil {
|
||||
|
@ -378,6 +374,13 @@ func SingleRelease(ctx *context.Context) {
|
|||
}
|
||||
|
||||
ctx.Data["Releases"] = releases
|
||||
|
||||
ctx.Data["OpenGraphTitle"] = fmt.Sprintf("%s - %s", release.DisplayName(), release.Repo.FullName())
|
||||
ctx.Data["OpenGraphDescription"] = base.EllipsisString(release.Note, 300)
|
||||
ctx.Data["OpenGraphURL"] = release.HTMLURL()
|
||||
ctx.Data["OpenGraphImageURL"] = release.SummaryCardURL()
|
||||
ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.release.summary_card_alt", release.DisplayName(), release.Repo.FullName())
|
||||
|
||||
ctx.HTML(http.StatusOK, tplReleasesList)
|
||||
}
|
||||
|
||||
|
|
|
@ -1146,9 +1146,10 @@ func registerRoutes(m *web.Route) {
|
|||
m.Group("/{type:issues|pulls}", func() {
|
||||
m.Group("/{index}", func() {
|
||||
m.Get("/info", repo.GetIssueInfo)
|
||||
m.Get("/summary-card", repo.GetSummaryCard)
|
||||
m.Get("/summary-card", repo.DrawIssueSummaryCard)
|
||||
})
|
||||
})
|
||||
m.Get("/-/summary-card", repo.DrawRepoSummaryCard)
|
||||
}, ignSignIn, context.RepoAssignment, context.UnitTypes()) // for "/{username}/{reponame}" which doesn't require authentication
|
||||
|
||||
// Grouping for those endpoints that do require authentication
|
||||
|
@ -1298,6 +1299,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("/latest", repo.LatestRelease)
|
||||
m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS)
|
||||
m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom)
|
||||
m.Get("/summary-card/*", repo.DrawReleaseSummaryCard)
|
||||
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed),
|
||||
repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true))
|
||||
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue