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

@@ -38,6 +38,8 @@ pub struct ProfileSummary {
pub email: Option<String>, pub email: Option<String>,
pub avatar_data_url: Option<String>, pub avatar_data_url: Option<String>,
pub avatar_icon: Option<String>, pub avatar_icon: Option<String>,
pub default_avatar_fill_color: Option<i64>,
pub default_avatar_stroke_color: Option<i64>,
pub avatar_label: String, pub avatar_label: String,
pub path: String, pub path: String,
} }
@@ -69,6 +71,8 @@ pub struct AssociatedProfileSummary {
pub name: String, pub name: String,
pub avatar_data_url: Option<String>, pub avatar_data_url: Option<String>,
pub avatar_icon: Option<String>, pub avatar_icon: Option<String>,
pub default_avatar_fill_color: Option<i64>,
pub default_avatar_stroke_color: Option<i64>,
pub avatar_label: String, pub avatar_label: String,
} }
@@ -79,6 +83,8 @@ pub struct BookmarkAssociatedProfileSummary {
pub name: String, pub name: String,
pub avatar_data_url: Option<String>, pub avatar_data_url: Option<String>,
pub avatar_icon: Option<String>, pub avatar_icon: Option<String>,
pub default_avatar_fill_color: Option<i64>,
pub default_avatar_stroke_color: Option<i64>,
pub avatar_label: String, pub avatar_label: String,
pub bookmark_path: String, pub bookmark_path: String,
} }

View File

@@ -159,6 +159,12 @@ fn build_profile_summary(
.and_then(Value::as_str) .and_then(Value::as_str)
.filter(|value| !value.is_empty()) .filter(|value| !value.is_empty())
.map(str::to_string); .map(str::to_string);
let default_avatar_fill_color = profile_info
.and_then(|value| value.get("default_avatar_fill_color"))
.and_then(Value::as_i64);
let default_avatar_stroke_color = profile_info
.and_then(|value| value.get("default_avatar_stroke_color"))
.and_then(Value::as_i64);
let avatar_label = name let avatar_label = name
.chars() .chars()
.find(|character| !character.is_whitespace()) .find(|character| !character.is_whitespace())
@@ -171,6 +177,8 @@ fn build_profile_summary(
email, email,
avatar_data_url, avatar_data_url,
avatar_icon, avatar_icon,
default_avatar_fill_color,
default_avatar_stroke_color,
avatar_label, avatar_label,
path: profile_path.display().to_string(), path: profile_path.display().to_string(),
} }
@@ -262,6 +270,8 @@ fn scan_extensions_for_profile(
name: profile.name.clone(), name: profile.name.clone(),
avatar_data_url: profile.avatar_data_url.clone(), avatar_data_url: profile.avatar_data_url.clone(),
avatar_icon: profile.avatar_icon.clone(), avatar_icon: profile.avatar_icon.clone(),
default_avatar_fill_color: profile.default_avatar_fill_color,
default_avatar_stroke_color: profile.default_avatar_stroke_color,
avatar_label: profile.avatar_label.clone(), avatar_label: profile.avatar_label.clone(),
}); });
} }
@@ -423,6 +433,8 @@ fn collect_bookmarks(
name: profile.name.clone(), name: profile.name.clone(),
avatar_data_url: profile.avatar_data_url.clone(), avatar_data_url: profile.avatar_data_url.clone(),
avatar_icon: profile.avatar_icon.clone(), avatar_icon: profile.avatar_icon.clone(),
default_avatar_fill_color: profile.default_avatar_fill_color,
default_avatar_stroke_color: profile.default_avatar_stroke_color,
avatar_label: profile.avatar_label.clone(), avatar_label: profile.avatar_label.clone(),
bookmark_path, bookmark_path,
}); });

View File

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

View File

@@ -41,9 +41,39 @@ const avatarMap = Object.entries(avatarModules).reduce<Record<string, Record<str
); );
type ProfileAvatarLike = type ProfileAvatarLike =
| Pick<ProfileSummary, "avatarDataUrl" | "avatarIcon"> | Pick<
| Pick<AssociatedProfileSummary, "avatarDataUrl" | "avatarIcon"> ProfileSummary,
| Pick<BookmarkAssociatedProfileSummary, "avatarDataUrl" | "avatarIcon">; | "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( export function profileAvatarSrc(
profile: ProfileAvatarLike, profile: ProfileAvatarLike,
@@ -54,13 +84,18 @@ export function profileAvatarSrc(
} }
const avatarKey = normalizeAvatarIcon(profile.avatarIcon); const avatarKey = normalizeAvatarIcon(profile.avatarIcon);
if (!avatarKey) { if (avatarKey) {
return null; const familyMap = browserFamilyId ? avatarMap[browserFamilyId] : undefined;
if (familyMap?.[avatarKey]) {
return familyMap[avatarKey];
}
} }
const familyMap = browserFamilyId ? avatarMap[browserFamilyId] : undefined; if (browserFamilyId === "chrome") {
if (familyMap?.[avatarKey]) { return createChromeGeneratedAvatar(
return familyMap[avatarKey]; profile.defaultAvatarFillColor,
profile.defaultAvatarStrokeColor,
);
} }
return null; return null;
@@ -74,3 +109,34 @@ function normalizeAvatarIcon(value: string | null | undefined) {
const lastSegment = withoutQuery.split("/").pop() ?? withoutQuery; const lastSegment = withoutQuery.split("/").pop() ?? withoutQuery;
return lastSegment.replace(/\.png$/i, ""); 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");
}