|
| 1 | +import {getActionStatusIcon} from './action-status-icon.ts'; |
| 2 | +import type {ActionsStatus} from './gitea-actions.ts'; |
| 3 | +import {svgParseOuterInner} from '../svg.ts'; |
| 4 | +import {html, htmlRaw} from '../utils/html.ts'; |
| 5 | + |
| 6 | +const {svgOuter, svgInnerHtml: giteaFaviconInner} = svgParseOuterInner('gitea-favicon'); |
| 7 | +const faviconViewBox = svgOuter.getAttribute('viewBox')!; |
| 8 | +const [, , faviconViewBoxWidth, faviconViewBoxHeight] = faviconViewBox.split(/\s+/).map(Number); |
| 9 | + |
| 10 | +// the status badge is rendered in the bottom-right corner, following GitHub Actions favicon proportions |
| 11 | +const badgeIconSize = 16; |
| 12 | +const badgeSizeRatio = 340 / 640; |
| 13 | +const badgeMargin = 6; |
| 14 | +const badgeDrawSize = faviconViewBoxWidth * badgeSizeRatio; |
| 15 | +const badgeX = faviconViewBoxWidth - badgeDrawSize - badgeMargin; |
| 16 | +const badgeY = faviconViewBoxHeight - badgeDrawSize - badgeMargin; |
| 17 | +const badgeScale = badgeDrawSize / badgeIconSize; |
| 18 | +// white ring behind the badge so it stands out from the logo, like GitHub's favicon |
| 19 | +const badgeCenter = badgeDrawSize / 2; |
| 20 | +const badgeRingRadius = badgeCenter + badgeDrawSize * 0.08; |
| 21 | + |
| 22 | +let currentStatus: ActionsStatus | null = null; |
| 23 | +const defaultFaviconHrefs = new Map<HTMLLinkElement, string>(); |
| 24 | +const faviconDataUrlCache = new Map<ActionsStatus, string>(); |
| 25 | +let colorProbe: HTMLElement | null = null; |
| 26 | + |
| 27 | +function rememberDefaultFaviconHrefs() { |
| 28 | + if (defaultFaviconHrefs.size > 0) return; |
| 29 | + for (const link of document.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]')) { |
| 30 | + defaultFaviconHrefs.set(link, link.href); |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +function resolveTailwindTextColor(colorClass: string): string { |
| 35 | + if (!colorProbe) { |
| 36 | + colorProbe = document.createElement('span'); |
| 37 | + colorProbe.style.display = 'none'; |
| 38 | + document.body.append(colorProbe); |
| 39 | + } |
| 40 | + colorProbe.className = colorClass; |
| 41 | + return getComputedStyle(colorProbe).color || '#000000'; |
| 42 | +} |
| 43 | + |
| 44 | +function buildStatusIconMarkup(status: ActionsStatus): string { |
| 45 | + const {name, colorClass} = getActionStatusIcon(status, 'circle-fill'); |
| 46 | + const color = resolveTailwindTextColor(colorClass); |
| 47 | + const {svgInnerHtml} = svgParseOuterInner(name); |
| 48 | + const coloredInner = svgInnerHtml.replaceAll('currentColor', color); |
| 49 | + const ring = html`<circle cx="${badgeX + badgeCenter}" cy="${badgeY + badgeCenter}" r="${badgeRingRadius}" fill="#ffffff"/>`; |
| 50 | + const badge = html`<g data-actions-status-name="${status}" transform="translate(${badgeX}, ${badgeY}) scale(${badgeScale})" fill="${color}" color="${color}">${htmlRaw(coloredInner)}</g>`; |
| 51 | + return html`${htmlRaw(ring)}${htmlRaw(badge)}`; |
| 52 | +} |
| 53 | + |
| 54 | +export function buildStatusFaviconSvg(status: ActionsStatus): string { |
| 55 | + return html`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${faviconViewBox}">${htmlRaw(giteaFaviconInner)}${htmlRaw(buildStatusIconMarkup(status))}</svg>`; |
| 56 | +} |
| 57 | + |
| 58 | +function buildStatusFaviconDataUrl(status: ActionsStatus): string { |
| 59 | + const cached = faviconDataUrlCache.get(status); |
| 60 | + if (cached) return cached; |
| 61 | + const dataUrl = `data:image/svg+xml,${encodeURIComponent(buildStatusFaviconSvg(status))}`; |
| 62 | + faviconDataUrlCache.set(status, dataUrl); |
| 63 | + return dataUrl; |
| 64 | +} |
| 65 | + |
| 66 | +function setFaviconHref(href: string) { |
| 67 | + rememberDefaultFaviconHrefs(); |
| 68 | + for (const link of defaultFaviconHrefs.keys()) { |
| 69 | + if (link.isConnected) link.href = href; |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +export function syncActionRunFavicon(status: ActionsStatus | ''): void { |
| 74 | + if (status === '') { |
| 75 | + resetActionFavicon(); |
| 76 | + return; |
| 77 | + } |
| 78 | + if (status === currentStatus) return; |
| 79 | + setFaviconHref(buildStatusFaviconDataUrl(status)); |
| 80 | + currentStatus = status; |
| 81 | +} |
| 82 | + |
| 83 | +export function resetActionFavicon(): void { |
| 84 | + if (currentStatus === null) return; |
| 85 | + rememberDefaultFaviconHrefs(); |
| 86 | + for (const [link, href] of defaultFaviconHrefs) { |
| 87 | + if (link.isConnected) link.href = href; |
| 88 | + } |
| 89 | + currentStatus = null; |
| 90 | +} |
0 commit comments