support color avatar

This commit is contained in:
Julian Freeman
2026-04-16 19:11:43 -04:00
parent 3e7bf3a7ce
commit aac78b4b9c
4 changed files with 98 additions and 8 deletions

View File

@@ -10,6 +10,8 @@ export type ProfileSummary = {
email: string | null;
avatarDataUrl: string | null;
avatarIcon: string | null;
defaultAvatarFillColor: number | null;
defaultAvatarStrokeColor: number | null;
avatarLabel: string;
path: string;
};
@@ -35,6 +37,8 @@ export type AssociatedProfileSummary = {
name: string;
avatarDataUrl: string | null;
avatarIcon: string | null;
defaultAvatarFillColor: number | null;
defaultAvatarStrokeColor: number | null;
avatarLabel: string;
};
@@ -43,6 +47,8 @@ export type BookmarkAssociatedProfileSummary = {
name: string;
avatarDataUrl: string | null;
avatarIcon: string | null;
defaultAvatarFillColor: number | null;
defaultAvatarStrokeColor: number | null;
avatarLabel: string;
bookmarkPath: string;
};

View File

@@ -41,9 +41,39 @@ const avatarMap = Object.entries(avatarModules).reduce<Record<string, Record<str
);
type ProfileAvatarLike =
| Pick<ProfileSummary, "avatarDataUrl" | "avatarIcon">
| Pick<AssociatedProfileSummary, "avatarDataUrl" | "avatarIcon">
| Pick<BookmarkAssociatedProfileSummary, "avatarDataUrl" | "avatarIcon">;
| Pick<
ProfileSummary,
| "avatarDataUrl"
| "avatarIcon"
| "defaultAvatarFillColor"
| "defaultAvatarStrokeColor"
>
| Pick<
AssociatedProfileSummary,
| "avatarDataUrl"
| "avatarIcon"
| "defaultAvatarFillColor"
| "defaultAvatarStrokeColor"
>
| Pick<
BookmarkAssociatedProfileSummary,
| "avatarDataUrl"
| "avatarIcon"
| "defaultAvatarFillColor"
| "defaultAvatarStrokeColor"
>;
const chromeProfileSvg = `
<svg version="1.1" width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<circle cx="48" cy="48" r="48" fill="#{bg_rgb}"/>
<g fill="#{fg_rgb}">
<circle cx="48" cy="32" r="12"/>
<path d="M24,68 C20,50 40,48 48,48 C56,48 76,50 72,68 Z"/>
</g>
</svg>
`.trim();
const chromeGeneratedAvatarCache = new Map<string, string>();
export function profileAvatarSrc(
profile: ProfileAvatarLike,
@@ -54,13 +84,18 @@ export function profileAvatarSrc(
}
const avatarKey = normalizeAvatarIcon(profile.avatarIcon);
if (!avatarKey) {
return null;
if (avatarKey) {
const familyMap = browserFamilyId ? avatarMap[browserFamilyId] : undefined;
if (familyMap?.[avatarKey]) {
return familyMap[avatarKey];
}
}
const familyMap = browserFamilyId ? avatarMap[browserFamilyId] : undefined;
if (familyMap?.[avatarKey]) {
return familyMap[avatarKey];
if (browserFamilyId === "chrome") {
return createChromeGeneratedAvatar(
profile.defaultAvatarFillColor,
profile.defaultAvatarStrokeColor,
);
}
return null;
@@ -74,3 +109,34 @@ function normalizeAvatarIcon(value: string | null | undefined) {
const lastSegment = withoutQuery.split("/").pop() ?? withoutQuery;
return lastSegment.replace(/\.png$/i, "");
}
function createChromeGeneratedAvatar(
backgroundArgb: number | null | undefined,
foregroundArgb: number | null | undefined,
) {
if (backgroundArgb == null || foregroundArgb == null) {
return null;
}
const cacheKey = `${backgroundArgb}:${foregroundArgb}`;
const cached = chromeGeneratedAvatarCache.get(cacheKey);
if (cached) {
return cached;
}
const backgroundRgb = argbToRgbHex(backgroundArgb);
const foregroundRgb = argbToRgbHex(foregroundArgb);
const svg = chromeProfileSvg
.replace("{bg_rgb}", backgroundRgb)
.replace("{fg_rgb}", foregroundRgb);
const dataUrl = `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
chromeGeneratedAvatarCache.set(cacheKey, dataUrl);
return dataUrl;
}
function argbToRgbHex(argbValue: number) {
const unsignedArgb = argbValue >>> 0;
const rgb = unsignedArgb & 0x00ff_ffff;
return rgb.toString(16).padStart(6, "0");
}