diff --git a/modules/socialcard/socialcard.go b/modules/socialcard/socialcard.go index 79dafccc76..e7b0e3ae98 100644 --- a/modules/socialcard/socialcard.go +++ b/modules/socialcard/socialcard.go @@ -213,12 +213,15 @@ func (r *Renderer) renderPresetCard(data CardData, theme Theme) ([]byte, error) } // Calculate total text block height for vertical centering + titleLineH := 76 // line height for 64px title + descLineH := 38 // line height for 28px desc + titleLines := wrapText(data.Title, faces.title, contentWidth) if len(titleLines) > 2 { titleLines = titleLines[:2] titleLines[1] = truncateWithEllipsis(titleLines[1], faces.title, contentWidth) } - titleBlockH := len(titleLines) * 60 + titleBlockH := len(titleLines) * titleLineH var descLines []string descBlockH := 0 @@ -228,25 +231,25 @@ func (r *Renderer) renderPresetCard(data CardData, theme Theme) ([]byte, error) descLines = descLines[:3] descLines[2] = truncateWithEllipsis(descLines[2], faces.desc, contentWidth) } - descBlockH = len(descLines)*34 + 16 // 16 = gap between title and desc + descBlockH = len(descLines)*descLineH + 16 } - metaH := 28 // height of bottom metadata line + metaH := 30 gapBeforeMeta := 24 totalH := titleBlockH + descBlockH + gapBeforeMeta + metaH // Vertically center the text block - startY := (CardHeight-totalH)/2 + 48 // baseline of first title line + startY := (CardHeight-totalH)/2 + 64 // baseline of first title line // Draw title for i, line := range titleLines { - drawText(img, leftPad, startY+i*60, line, faces.title, theme.TitleColor) + drawText(img, leftPad, startY+i*titleLineH, line, faces.title, theme.TitleColor) } // Draw description descY := startY + titleBlockH + 16 for i, line := range descLines { - drawText(img, leftPad, descY+i*34, line, faces.desc, theme.DescColor) + drawText(img, leftPad, descY+i*descLineH, line, faces.desc, theme.DescColor) } // Bottom metadata: language + repo name @@ -294,7 +297,7 @@ func (r *Renderer) renderSolidCard(data CardData) ([]byte, error) { cropped, cropped.Bounds().Min, draw.Src) } - faces, err := r.createFaces() + faces, err := r.createPortraitFaces() if err != nil { return nil, err } @@ -384,7 +387,7 @@ func (r *Renderer) renderImageCard(data CardData) ([]byte, error) { descColor := color.RGBA{R: 230, G: 230, B: 230, A: 255} subtextColor := color.RGBA{R: 170, G: 170, B: 170, A: 255} - faces, err := r.createFaces() + faces, err := r.createPortraitFaces() if err != nil { return nil, err } @@ -467,21 +470,29 @@ func (f *fontFaces) close() { } func (r *Renderer) createFaces() (*fontFaces, error) { + return r.createFacesWithSizes(64, 28, 22) +} + +func (r *Renderer) createPortraitFaces() (*fontFaces, error) { + return r.createFacesWithSizes(56, 28, 22) +} + +func (r *Renderer) createFacesWithSizes(titleSize, descSize, metaSize float64) (*fontFaces, error) { titleFace, err := opentype.NewFace(r.boldFont, &opentype.FaceOptions{ - Size: 48, DPI: 72, Hinting: font.HintingFull, + Size: titleSize, DPI: 72, Hinting: font.HintingFull, }) if err != nil { return nil, fmt.Errorf("create title face: %w", err) } descFace, err := opentype.NewFace(r.regularFont, &opentype.FaceOptions{ - Size: 24, DPI: 72, Hinting: font.HintingFull, + Size: descSize, DPI: 72, Hinting: font.HintingFull, }) if err != nil { titleFace.Close() return nil, fmt.Errorf("create desc face: %w", err) } metaFace, err := opentype.NewFace(r.regularFont, &opentype.FaceOptions{ - Size: 20, DPI: 72, Hinting: font.HintingFull, + Size: metaSize, DPI: 72, Hinting: font.HintingFull, }) if err != nil { titleFace.Close() @@ -568,7 +579,8 @@ func drawCircle(img *image.RGBA, cx, cy, radius int, c color.RGBA) { } } -// drawRoundedAvatar draws an avatar image clipped to a rounded rectangle. +// drawRoundedAvatar draws an avatar image clipped to a rounded rectangle +// using proper alpha compositing so transparent areas show the card background. func drawRoundedAvatar(dst *image.RGBA, x, y, size, radius int, avatar image.Image) { scaled := scaleImage(avatar, size, size) @@ -577,13 +589,31 @@ func drawRoundedAvatar(dst *image.RGBA, x, y, size, radius int, avatar image.Ima if !insideRoundedRect(px, py, size, size, radius) { continue } - r, g, b, a := scaled.At(px, py).RGBA() - dst.SetRGBA(x+px, y+py, color.RGBA{ - R: uint8(r >> 8), - G: uint8(g >> 8), - B: uint8(b >> 8), - A: uint8(a >> 8), - }) + sr, sg, sb, sa := scaled.At(px, py).RGBA() + if sa == 0 { + continue // fully transparent — skip + } + dx, dy := x+px, y+py + if sa == 0xFFFF { + // Fully opaque — direct write + dst.SetRGBA(dx, dy, color.RGBA{ + R: uint8(sr >> 8), G: uint8(sg >> 8), + B: uint8(sb >> 8), A: 255, + }) + } else { + // Alpha composite: dst = src over dst + dr, dg, db, da := dst.At(dx, dy).RGBA() + a := sa + invA := 0xFFFF - a + outR := (sr*a + dr*invA) / 0xFFFF + outG := (sg*a + dg*invA) / 0xFFFF + outB := (sb*a + db*invA) / 0xFFFF + outA := a + (da*invA)/0xFFFF + dst.SetRGBA(dx, dy, color.RGBA{ + R: uint8(outR >> 8), G: uint8(outG >> 8), + B: uint8(outB >> 8), A: uint8(outA >> 8), + }) + } } } } diff --git a/routers/web/web.go b/routers/web/web.go index b43eaaf28b..27a959ce2d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1198,7 +1198,7 @@ func registerWebRoutes(m *web.Router) { m.Group("/media_kit", func() { m.Combo("").Get(repo_setting.MediaKit).Post(repo_setting.MediaKitPost) m.Post("/upload", repo_setting.MediaKitUploadImage) - m.Delete("/image", repo_setting.MediaKitDeleteImage) + m.Post("/delete_image", repo_setting.MediaKitDeleteImage) m.Get("/unsplash/search", repo_setting.MediaKitUnsplashSearch) m.Post("/unsplash/select", repo_setting.MediaKitUnsplashSelect) }) diff --git a/templates/repo/settings/media_kit.tmpl b/templates/repo/settings/media_kit.tmpl index fbcd86f3a2..a6838ff263 100644 --- a/templates/repo/settings/media_kit.tmpl +++ b/templates/repo/settings/media_kit.tmpl @@ -74,7 +74,7 @@ {{svg "octicon-image" 14}} Custom uploaded image {{end}} - @@ -191,18 +191,38 @@ loadingDiv.classList.add('tw-hidden'); photos.forEach(function(photo) { const el = document.createElement('div'); - el.style.cssText = 'cursor:pointer;border-radius:6px;overflow:hidden;border:2px solid transparent;'; - el.innerHTML = ''; + el.style.cssText = 'cursor:pointer;border-radius:6px;overflow:hidden;border:2px solid transparent;position:relative;'; + el.innerHTML = '' + + '
' + photo.author + '
'; el.addEventListener('click', async function() { + // Deselect all others + resultsDiv.querySelectorAll('div').forEach(function(d) { d.style.borderColor = 'transparent'; }); el.style.borderColor = 'var(--color-primary)'; - const csrf = document.querySelector('input[name="_csrf"]').value; - const fd = new FormData(); - fd.append('_csrf', csrf); - fd.append('id', photo.id); - fd.append('author', photo.author); - fd.append('url', photo.regular); - await fetch('{{.Link}}/unsplash/select', {method: 'POST', body: fd}); - updatePreview(); + el.style.opacity = '0.7'; + el.style.pointerEvents = 'none'; + try { + const csrf = document.querySelector('input[name="_csrf"]').value; + const fd = new FormData(); + fd.append('_csrf', csrf); + fd.append('id', photo.id); + fd.append('author', photo.author); + fd.append('url', photo.regular); + const resp = await fetch('{{.Link}}/unsplash/select', {method: 'POST', body: fd}); + if (resp.ok) { + // Switch radio to "image" and update UI + const imageRadio = document.querySelector('.style-radio[value="image"]'); + if (imageRadio) { + imageRadio.checked = true; + updateButtonStyles(); + solidOpts.style.display = 'none'; + imageOpts.style.display = ''; + } + updatePreview(); + } + } finally { + el.style.opacity = '1'; + el.style.pointerEvents = ''; + } }); resultsDiv.appendChild(el); });