feat(contacts): revamp edit dialog with live preview + avatar picker

Rebuild the add/edit contact dialog on the portfolio-editor design language:

- A live preview card at the top shows the contact exactly as it renders in the
  list (avatar + name + address), updating as you type and pick an avatar.
- An avatar picker (Badge / Icon / Image segmented control) lets you keep the
  default Z/T type badge, choose a Material wallet icon from a searchable grid,
  or set a custom image. The picker area is fixed-height so the modal doesn't
  jump when switching modes.
- Custom images go through a new in-app ImagePicker (image_picker.h): a
  Material overlay that browses the filesystem starting at the user's Pictures
  folder, shows a thumbnail grid (decoded to raw pixels, box-downscaled to a
  small texture, cached per directory and freed on navigate/close, budgeted a
  few decodes per frame so large folders don't hitch), and returns the chosen
  path. The chosen image is copied into <config>/contact-avatars/ (named by an
  FNV hash of the source path, so re-picking is idempotent) and stored as
  "img:<path>". Like FolderPicker, it takes over the modal surface while open.
- Paste is now available on both add and edit; buttons are content-sized.

Adds three sweep surfaces (contacts-edit-{icon,badge,image}) that open the
dialog on a seeded contact in each avatar mode, plus i18n keys (+ 8-language
translations; reworded two zh/ja strings to stay within the existing CJK
subset, so no font rebuild).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 07:10:00 -05:00
parent 2249bc31d5
commit 2d4ba89c00
13 changed files with 809 additions and 25 deletions

View File

@@ -306,6 +306,13 @@ void I18n::loadBuiltinEnglish()
strings_["picker_dat_count"] = "%d wallet file(s) in this folder";
strings_["picker_dat_none"] = "No wallet files directly in this folder";
strings_["picker_select"] = "Scan this folder";
// In-app image picker (contact avatars)
strings_["img_picker_title"] = "Choose an image";
strings_["img_picker_pictures"] = "Pictures folder";
strings_["img_picker_empty"] = "This folder has no sub-folders or images.";
strings_["img_picker_none"] = "No images in this folder";
strings_["img_picker_count"] = "%d image(s) in this folder";
strings_["img_picker_use"] = "Use image";
strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it";
// Migrate-to-seed modal
strings_["mig_title"] = "Migrate to a seed wallet";
@@ -1222,6 +1229,18 @@ void I18n::loadBuiltinEnglish()
strings_["address_book_add_new"] = "Add New";
strings_["contact_global"] = "Show in every wallet (global contact)";
strings_["contact_global_tt"] = "On: this contact stays visible no matter which wallet you load. Off: it belongs to the current wallet only.";
// Contact edit dialog: live preview + avatar picker
strings_["contact_preview_name"] = "Contact name";
strings_["contact_preview_addr"] = "Address will appear here";
strings_["contact_avatar"] = "AVATAR";
strings_["contact_avatar_badge"] = "Badge";
strings_["contact_avatar_icon"] = "Icon";
strings_["contact_avatar_image"] = "Image";
strings_["contact_avatar_badge_hint"] = "Uses the address-type badge — Z for shielded, T for transparent.";
strings_["contact_avatar_choose"] = "Choose image\xE2\x80\xA6";
strings_["contact_avatar_remove"] = "Remove";
strings_["contact_avatar_image_hint"] = "The image is copied into the app so it stays available if the original moves.";
strings_["contact_avatar_copy_failed"] = "Could not copy that image.";
strings_["contact_global_badge_tt"] = "Global contact — visible in every wallet";
strings_["address_book_added"] = "Address added to book";
strings_["address_book_count"] = "%zu addresses saved";