mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	Add flat-square action badge style (#34062)
Adds the `flat-square` style to action badges. Styles can be selected by adding `?style=<style>` to the badge endpoint. If no style query is given, or if the query is invalid, the style defaults to `flat`. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -5,6 +5,7 @@ package badge | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"unicode" | 	"unicode" | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| @@ -49,14 +50,27 @@ func (b Badge) Width() int { | |||||||
| 	return b.Label.width + b.Message.width | 	return b.Label.width + b.Message.width | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Style follows https://shields.io/badges | ||||||
|  | const ( | ||||||
|  | 	StyleFlat       = "flat" | ||||||
|  | 	StyleFlatSquare = "flat-square" | ||||||
|  | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	defaultOffset     = 10 | 	defaultOffset     = 10 | ||||||
| 	defaultFontSize   = 11 | 	defaultFontSize   = 11 | ||||||
| 	DefaultColor      = "#9f9f9f" // Grey | 	DefaultColor      = "#9f9f9f" // Grey | ||||||
| 	DefaultFontFamily = "DejaVu Sans,Verdana,Geneva,sans-serif" | 	DefaultFontFamily = "DejaVu Sans,Verdana,Geneva,sans-serif" | ||||||
|  | 	DefaultStyle      = StyleFlat | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var StatusColorMap = map[actions_model.Status]string{ | var GlobalVars = sync.OnceValue(func() (ret struct { | ||||||
|  | 	StatusColorMap       map[actions_model.Status]string | ||||||
|  | 	DejaVuGlyphWidthData map[rune]uint8 | ||||||
|  | 	AllStyles            []string | ||||||
|  | }, | ||||||
|  | ) { | ||||||
|  | 	ret.StatusColorMap = map[actions_model.Status]string{ | ||||||
| 		actions_model.StatusSuccess:   "#4c1",    // Green | 		actions_model.StatusSuccess:   "#4c1",    // Green | ||||||
| 		actions_model.StatusSkipped:   "#dfb317", // Yellow | 		actions_model.StatusSkipped:   "#dfb317", // Yellow | ||||||
| 		actions_model.StatusUnknown:   "#97ca00", // Light Green | 		actions_model.StatusUnknown:   "#97ca00", // Light Green | ||||||
| @@ -65,7 +79,11 @@ var StatusColorMap = map[actions_model.Status]string{ | |||||||
| 		actions_model.StatusWaiting:   "#dfb317", // Yellow | 		actions_model.StatusWaiting:   "#dfb317", // Yellow | ||||||
| 		actions_model.StatusRunning:   "#dfb317", // Yellow | 		actions_model.StatusRunning:   "#dfb317", // Yellow | ||||||
| 		actions_model.StatusBlocked:   "#dfb317", // Yellow | 		actions_model.StatusBlocked:   "#dfb317", // Yellow | ||||||
| } | 	} | ||||||
|  | 	ret.DejaVuGlyphWidthData = dejaVuGlyphWidthDataFunc() | ||||||
|  | 	ret.AllStyles = []string{StyleFlat, StyleFlatSquare} | ||||||
|  | 	return ret | ||||||
|  | }) | ||||||
|  |  | ||||||
| // GenerateBadge generates badge with given template | // GenerateBadge generates badge with given template | ||||||
| func GenerateBadge(label, message, color string) Badge { | func GenerateBadge(label, message, color string) Badge { | ||||||
| @@ -93,7 +111,7 @@ func GenerateBadge(label, message, color string) Badge { | |||||||
|  |  | ||||||
| func calculateTextWidth(text string) int { | func calculateTextWidth(text string) int { | ||||||
| 	width := 0 | 	width := 0 | ||||||
| 	widthData := DejaVuGlyphWidthData() | 	widthData := GlobalVars().DejaVuGlyphWidthData | ||||||
| 	for _, char := range strings.TrimSpace(text) { | 	for _, char := range strings.TrimSpace(text) { | ||||||
| 		charWidth, ok := widthData[char] | 		charWidth, ok := widthData[char] | ||||||
| 		if !ok { | 		if !ok { | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ | |||||||
|  |  | ||||||
| package badge | package badge | ||||||
|  |  | ||||||
| import "sync" |  | ||||||
|  |  | ||||||
| // DejaVuGlyphWidthData is generated by `sfnt.Face.GlyphAdvance(nil, <rune>, 11, font.HintingNone)` with DejaVu Sans | // DejaVuGlyphWidthData is generated by `sfnt.Face.GlyphAdvance(nil, <rune>, 11, font.HintingNone)` with DejaVu Sans | ||||||
| // v2.37 (https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-sans-ttf-2.37.zip). | // v2.37 (https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-sans-ttf-2.37.zip). | ||||||
| // | // | ||||||
| @@ -13,7 +11,7 @@ import "sync" | |||||||
| // | // | ||||||
| // A devtest page "/devtest/badge-actions-svg" could be used to check the rendered images. | // A devtest page "/devtest/badge-actions-svg" could be used to check the rendered images. | ||||||
|  |  | ||||||
| var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 { | func dejaVuGlyphWidthDataFunc() map[rune]uint8 { | ||||||
| 	return map[rune]uint8{ | 	return map[rune]uint8{ | ||||||
| 		32:  3, | 		32:  3, | ||||||
| 		33:  4, | 		33:  4, | ||||||
| @@ -205,4 +203,4 @@ var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 { | |||||||
| 		254: 7, | 		254: 7, | ||||||
| 		255: 7, | 		255: 7, | ||||||
| 	} | 	} | ||||||
| }) | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| package devtest | package devtest | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path" | 	"path" | ||||||
| @@ -128,6 +129,7 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { | |||||||
| func prepareMockDataBadgeActionsSvg(ctx *context.Context) { | func prepareMockDataBadgeActionsSvg(ctx *context.Context) { | ||||||
| 	fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",") | 	fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",") | ||||||
| 	selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0]) | 	selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0]) | ||||||
|  | 	selectedStyle := ctx.FormString("style", badge.DefaultStyle) | ||||||
| 	var badges []badge.Badge | 	var badges []badge.Badge | ||||||
| 	badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green")) | 	badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green")) | ||||||
| 	for r := rune(0); r < 256; r++ { | 	for r := rune(0); r < 256; r++ { | ||||||
| @@ -141,7 +143,16 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) { | |||||||
| 	for i, b := range badges { | 	for i, b := range badges { | ||||||
| 		b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-" | 		b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-" | ||||||
| 		b.FontFamily = selectedFontFamilyName | 		b.FontFamily = selectedFontFamilyName | ||||||
| 		h, err := ctx.RenderToHTML("shared/actions/runner_badge", map[string]any{"Badge": b}) | 		var h template.HTML | ||||||
|  | 		var err error | ||||||
|  | 		switch selectedStyle { | ||||||
|  | 		case badge.StyleFlat: | ||||||
|  | 			h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat", map[string]any{"Badge": b}) | ||||||
|  | 		case badge.StyleFlatSquare: | ||||||
|  | 			h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat-square", map[string]any{"Badge": b}) | ||||||
|  | 		default: | ||||||
|  | 			err = fmt.Errorf("unknown badge style: %s", selectedStyle) | ||||||
|  | 		} | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.ServerError("RenderToHTML", err) | 			ctx.ServerError("RenderToHTML", err) | ||||||
| 			return | 			return | ||||||
| @@ -151,6 +162,8 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) { | |||||||
| 	ctx.Data["BadgeSVGs"] = badgeSVGs | 	ctx.Data["BadgeSVGs"] = badgeSVGs | ||||||
| 	ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames | 	ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames | ||||||
| 	ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName | 	ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName | ||||||
|  | 	ctx.Data["BadgeStyles"] = badge.GlobalVars().AllStyles | ||||||
|  | 	ctx.Data["SelectedStyle"] = selectedStyle | ||||||
| } | } | ||||||
|  |  | ||||||
| func prepareMockData(ctx *context.Context) { | func prepareMockData(ctx *context.Context) { | ||||||
|   | |||||||
| @@ -5,35 +5,38 @@ package actions | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| 	"code.gitea.io/gitea/modules/badge" | 	"code.gitea.io/gitea/modules/badge" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/services/context" | 	"code.gitea.io/gitea/services/context" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetWorkflowBadge(ctx *context.Context) { | func GetWorkflowBadge(ctx *context.Context) { | ||||||
| 	workflowFile := ctx.PathParam("workflow_name") | 	workflowFile := ctx.PathParam("workflow_name") | ||||||
| 	branch := ctx.Req.URL.Query().Get("branch") | 	branch := ctx.FormString("branch", ctx.Repo.Repository.DefaultBranch) | ||||||
| 	if branch == "" { | 	event := ctx.FormString("event") | ||||||
| 		branch = ctx.Repo.Repository.DefaultBranch | 	style := ctx.FormString("style") | ||||||
| 	} |  | ||||||
| 	branchRef := fmt.Sprintf("refs/heads/%s", branch) |  | ||||||
| 	event := ctx.Req.URL.Query().Get("event") |  | ||||||
|  |  | ||||||
| 	badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event) | 	branchRef := git.RefNameFromBranch(branch) | ||||||
|  | 	b, err := getWorkflowBadge(ctx, workflowFile, branchRef.String(), event) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetWorkflowBadge", err) | 		ctx.ServerError("GetWorkflowBadge", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.Data["Badge"] = badge | 	ctx.Data["Badge"] = b | ||||||
| 	ctx.RespHeader().Set("Content-Type", "image/svg+xml") | 	ctx.RespHeader().Set("Content-Type", "image/svg+xml") | ||||||
| 	ctx.HTML(http.StatusOK, "shared/actions/runner_badge") | 	switch style { | ||||||
|  | 	case badge.StyleFlatSquare: | ||||||
|  | 		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat-square") | ||||||
|  | 	default: // defaults to badge.StyleFlat | ||||||
|  | 		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat") | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) { | func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) { | ||||||
| @@ -48,7 +51,7 @@ func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event stri | |||||||
| 		return badge.Badge{}, err | 		return badge.Badge{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	color, ok := badge.StatusColorMap[run.Status] | 	color, ok := badge.GlobalVars().StatusColorMap[run.Status] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil | 		return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -3,9 +3,16 @@ | |||||||
| 	<div> | 	<div> | ||||||
| 		<h1>Actions SVG</h1> | 		<h1>Actions SVG</h1> | ||||||
| 		<form class="tw-my-3"> | 		<form class="tw-my-3"> | ||||||
|  | 			<div class="tw-mb-2"> | ||||||
| 				{{range $fontName := .BadgeFontFamilyNames}} | 				{{range $fontName := .BadgeFontFamilyNames}} | ||||||
| 					<label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label> | 					<label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label> | ||||||
| 				{{end}} | 				{{end}} | ||||||
|  | 			</div> | ||||||
|  | 			<div class="tw-mb-2"> | ||||||
|  | 				{{range $style := .BadgeStyles}} | ||||||
|  | 					<label><input name="style" type="radio" value="{{$style}}" {{Iif (eq $.SelectedStyle $style) "checked"}}>{{$style}}</label> | ||||||
|  | 				{{end}} | ||||||
|  | 			</div> | ||||||
| 			<button>submit</button> | 			<button>submit</button> | ||||||
| 		</form> | 		</form> | ||||||
| 		<div class="flex-text-block tw-flex-wrap"> | 		<div class="flex-text-block tw-flex-wrap"> | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								templates/shared/actions/runner_badge_flat-square.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								templates/shared/actions/runner_badge_flat-square.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="20" | ||||||
|  | 	role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}"> | ||||||
|  | 	<title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title> | ||||||
|  | 	<g shape-rendering="crispEdges"> | ||||||
|  | 		<rect width="{{.Badge.Label.Width}}" height="20" fill="#555" /> | ||||||
|  | 		<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="20" fill="{{.Badge.Color}}" /> | ||||||
|  | 	</g> | ||||||
|  | 	<g fill="#fff" text-anchor="middle" font-family="{{.Badge.FontFamily}}" | ||||||
|  | 		text-rendering="geometricPrecision" font-size="{{.Badge.FontSize}}"> | ||||||
|  | 		<text x="{{.Badge.Label.X}}" y="140" | ||||||
|  | 			transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text> | ||||||
|  | 		<text x="{{.Badge.Message.X}}" y="140" transform="scale(.1)" fill="#fff" | ||||||
|  | 			textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text> | ||||||
|  | 	</g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 924 B | 
| Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB | 
		Reference in New Issue
	
	Block a user