From aac78b4b9c2a8da4d38eba1f7e9011449195d98f Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Thu, 16 Apr 2026 19:11:43 -0400 Subject: [PATCH] support color avatar --- src-tauri/src/models.rs | 6 +++ src-tauri/src/scanner.rs | 12 ++++++ src/types/browser.ts | 6 +++ src/utils/icons.ts | 82 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index d98f752..98d2d55 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -38,6 +38,8 @@ pub struct ProfileSummary { pub email: Option, pub avatar_data_url: Option, pub avatar_icon: Option, + pub default_avatar_fill_color: Option, + pub default_avatar_stroke_color: Option, pub avatar_label: String, pub path: String, } @@ -69,6 +71,8 @@ pub struct AssociatedProfileSummary { pub name: String, pub avatar_data_url: Option, pub avatar_icon: Option, + pub default_avatar_fill_color: Option, + pub default_avatar_stroke_color: Option, pub avatar_label: String, } @@ -79,6 +83,8 @@ pub struct BookmarkAssociatedProfileSummary { pub name: String, pub avatar_data_url: Option, pub avatar_icon: Option, + pub default_avatar_fill_color: Option, + pub default_avatar_stroke_color: Option, pub avatar_label: String, pub bookmark_path: String, } diff --git a/src-tauri/src/scanner.rs b/src-tauri/src/scanner.rs index 14b95bb..e755d7f 100644 --- a/src-tauri/src/scanner.rs +++ b/src-tauri/src/scanner.rs @@ -159,6 +159,12 @@ fn build_profile_summary( .and_then(Value::as_str) .filter(|value| !value.is_empty()) .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 .chars() .find(|character| !character.is_whitespace()) @@ -171,6 +177,8 @@ fn build_profile_summary( email, avatar_data_url, avatar_icon, + default_avatar_fill_color, + default_avatar_stroke_color, avatar_label, path: profile_path.display().to_string(), } @@ -262,6 +270,8 @@ fn scan_extensions_for_profile( name: profile.name.clone(), avatar_data_url: profile.avatar_data_url.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(), }); } @@ -423,6 +433,8 @@ fn collect_bookmarks( name: profile.name.clone(), avatar_data_url: profile.avatar_data_url.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(), bookmark_path, }); diff --git a/src/types/browser.ts b/src/types/browser.ts index dd1e138..19cd478 100644 --- a/src/types/browser.ts +++ b/src/types/browser.ts @@ -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; }; diff --git a/src/utils/icons.ts b/src/utils/icons.ts index 104c050..a4f5796 100644 --- a/src/utils/icons.ts +++ b/src/utils/icons.ts @@ -41,9 +41,39 @@ const avatarMap = Object.entries(avatarModules).reduce - | Pick - | Pick; + | Pick< + ProfileSummary, + | "avatarDataUrl" + | "avatarIcon" + | "defaultAvatarFillColor" + | "defaultAvatarStrokeColor" + > + | Pick< + AssociatedProfileSummary, + | "avatarDataUrl" + | "avatarIcon" + | "defaultAvatarFillColor" + | "defaultAvatarStrokeColor" + > + | Pick< + BookmarkAssociatedProfileSummary, + | "avatarDataUrl" + | "avatarIcon" + | "defaultAvatarFillColor" + | "defaultAvatarStrokeColor" + >; + +const chromeProfileSvg = ` + + + + + + + +`.trim(); + +const chromeGeneratedAvatarCache = new Map(); 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"); +}