feat(contacts): contact avatars — custom image / Material icon / Z-T badge

Add an `avatar` field to AddressBookEntry ("" = default Z/T type badge,
"icon:<name>" = a Material wallet-icon, "img:<path>" = a custom image),
serialized additively in addressbook.json (only written when non-empty, so
existing books are untouched).

Render it in the Cards/List views via a new drawContactAvatar helper: custom
images are loaded once through a path-keyed texture cache and drawn
circular-cropped (centre-cropped UVs + a thin border ring); icons reuse the
project-icon set (incl. the special pickaxe font path) in a tinted circle;
everything else falls back to the existing Z/T badge (also the fallback when
an image fails to load or an icon name is unknown).

Seed one sweep contact with an icon avatar to exercise the icon-badge path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 06:49:52 -05:00
co-authored by Claude Opus 4.8
parent 1ce37aa0fa
commit 2249bc31d5
4 changed files with 60 additions and 6 deletions
+2
View File
@@ -54,6 +54,7 @@ bool AddressBook::load()
// Legacy entries (no "scope") migrate to "global" so nothing disappears when
// multi-wallet scoping lands — a contact you already had stays visible everywhere.
e.scope = entry.value("scope", "global");
e.avatar = entry.value("avatar", "");
if (!e.address.empty()) {
entries_.push_back(e);
@@ -86,6 +87,7 @@ bool AddressBook::save()
e["address"] = entry.address;
e["notes"] = entry.notes;
e["scope"] = entry.scope.empty() ? std::string("global") : entry.scope;
if (!entry.avatar.empty()) e["avatar"] = entry.avatar;
j["entries"].push_back(e);
}
+3
View File
@@ -21,6 +21,9 @@ struct AddressBookEntry {
// (shown only when that wallet is the active one). Empty is treated as "global" so legacy
// entries — written before multi-wallet scoping — keep showing everywhere.
std::string scope = "global";
// Contact avatar: "" = default type badge (Z/T), "icon:<name>" = a Material wallet-icon,
// "img:<path>" = a custom image (copied into <config>/contact-avatars/).
std::string avatar;
AddressBookEntry() = default;
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "",