From 1b3446e43cad3ee3271cb178750f6fee5cdcc701 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 15 Jul 2026 08:07:07 -0500 Subject: [PATCH] feat(contacts): richer edit-dialog avatar picker (preview, chips, image, icons) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four refinements now that the two-column layout has room: 1. Image mode shows a large circular preview of the chosen picture (or a placeholder circle with an add-photo glyph, or a broken-image glyph if the file went missing), the filename, and centered Choose/Remove buttons — instead of a bare button. 2. The live-preview avatar is larger (r20 → r26) and the address is now middle-truncated (head + tail) so both ends read, like the real list row. 3. Badge mode shows the two actual chips — Z (shielded) and T (transparent) — with labels, making clear the badge is auto-picked from the address type, rather than a line of text. 4. The Badge/Icon/Image segmented control gains glyphs (badge / palette / image) beside its labels via a small two-font inline control (the shared SegmentedControl helper is single-font). Adds contact_avatar_shielded / _transparent keys and rewords the badge hint (the chips now carry the Z/T meaning); +8-language translations, all within the existing CJK subset. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/lang/de.json | 4 +- res/lang/es.json | 4 +- res/lang/fr.json | 4 +- res/lang/ja.json | 4 +- res/lang/ko.json | 4 +- res/lang/pt.json | 4 +- res/lang/ru.json | 4 +- res/lang/zh.json | 4 +- src/ui/windows/contacts_tab.cpp | 175 ++++++++++++++++++++++++++------ src/util/i18n.cpp | 4 +- 10 files changed, 170 insertions(+), 41 deletions(-) diff --git a/res/lang/de.json b/res/lang/de.json index ca8f183..64e14ee 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -289,13 +289,15 @@ "console_zoom_out": "Verkleinern", "contact_avatar": "AVATAR", "contact_avatar_badge": "Abzeichen", - "contact_avatar_badge_hint": "Verwendet das Adresstyp-Abzeichen – Z für abgeschirmt, T für transparent.", + "contact_avatar_badge_hint": "Das Abzeichen wird automatisch anhand des Adresstyps gewählt.", "contact_avatar_choose": "Bild auswählen…", "contact_avatar_copy_failed": "Dieses Bild konnte nicht kopiert werden.", "contact_avatar_icon": "Symbol", "contact_avatar_image": "Bild", "contact_avatar_image_hint": "Das Bild wird in die App kopiert, damit es verfügbar bleibt, wenn das Original verschoben wird.", "contact_avatar_remove": "Entfernen", + "contact_avatar_shielded": "Abgeschirmt", + "contact_avatar_transparent": "Transparent", "contact_global": "In jeder Wallet anzeigen (globaler Kontakt)", "contact_global_badge_tt": "Globaler Kontakt — in jeder Wallet sichtbar", "contact_global_tt": "Ein: Dieser Kontakt bleibt sichtbar, egal welche Wallet Sie laden. Aus: Er gehört nur zur aktuellen Wallet.", diff --git a/res/lang/es.json b/res/lang/es.json index 41e2f54..7f652df 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -289,13 +289,15 @@ "console_zoom_out": "Alejar", "contact_avatar": "AVATAR", "contact_avatar_badge": "Insignia", - "contact_avatar_badge_hint": "Usa la insignia de tipo de dirección: Z para blindada, T para transparente.", + "contact_avatar_badge_hint": "La insignia se elige automáticamente según el tipo de dirección.", "contact_avatar_choose": "Elegir imagen…", "contact_avatar_copy_failed": "No se pudo copiar esa imagen.", "contact_avatar_icon": "Icono", "contact_avatar_image": "Imagen", "contact_avatar_image_hint": "La imagen se copia en la app para que siga disponible si el original se mueve.", "contact_avatar_remove": "Quitar", + "contact_avatar_shielded": "Blindada", + "contact_avatar_transparent": "Transparente", "contact_global": "Mostrar en todas las carteras (contacto global)", "contact_global_badge_tt": "Contacto global — visible en todas las carteras", "contact_global_tt": "Activado: este contacto permanece visible sin importar qué cartera cargues. Desactivado: pertenece solo a la cartera actual.", diff --git a/res/lang/fr.json b/res/lang/fr.json index 1722af1..8380774 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -289,13 +289,15 @@ "console_zoom_out": "Réduire", "contact_avatar": "AVATAR", "contact_avatar_badge": "Badge", - "contact_avatar_badge_hint": "Utilise le badge de type d'adresse — Z pour blindée, T pour transparente.", + "contact_avatar_badge_hint": "Le badge est choisi automatiquement selon le type d'adresse.", "contact_avatar_choose": "Choisir une image…", "contact_avatar_copy_failed": "Impossible de copier cette image.", "contact_avatar_icon": "Icône", "contact_avatar_image": "Image", "contact_avatar_image_hint": "L'image est copiée dans l'application pour rester disponible si l'original est déplacé.", "contact_avatar_remove": "Retirer", + "contact_avatar_shielded": "Blindée", + "contact_avatar_transparent": "Transparente", "contact_global": "Afficher dans tous les portefeuilles (contact global)", "contact_global_badge_tt": "Contact global — visible dans tous les portefeuilles", "contact_global_tt": "Activé : ce contact reste visible quel que soit le portefeuille chargé. Désactivé : il appartient au portefeuille actuel uniquement.", diff --git a/res/lang/ja.json b/res/lang/ja.json index 4b17a9c..14bc2a3 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -289,13 +289,15 @@ "console_zoom_out": "縮小", "contact_avatar": "アバター", "contact_avatar_badge": "バッジ", - "contact_avatar_badge_hint": "アドレス種別バッジを使用します — Z はシールド、T は透明。", + "contact_avatar_badge_hint": "バッジはアドレスの種類に応じて自動的に選ばれます。", "contact_avatar_choose": "画像を選択…", "contact_avatar_copy_failed": "その画像をコピーできませんでした。", "contact_avatar_icon": "アイコン", "contact_avatar_image": "画像", "contact_avatar_image_hint": "画像はアプリ内にコピーされ、元のファイルが移動しても利用できます。", "contact_avatar_remove": "削除", + "contact_avatar_shielded": "シールド", + "contact_avatar_transparent": "透明", "contact_global": "すべてのウォレットで表示(グローバル連絡先)", "contact_global_badge_tt": "グローバル連絡先 — すべてのウォレットで表示", "contact_global_tt": "オン:この連絡先はどのウォレットを読み込んでも表示されます。オフ:現在のウォレットにのみ属します。", diff --git a/res/lang/ko.json b/res/lang/ko.json index e3789fe..1fa3e3a 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -289,13 +289,15 @@ "console_zoom_out": "축소", "contact_avatar": "아바타", "contact_avatar_badge": "배지", - "contact_avatar_badge_hint": "주소 유형 배지를 사용합니다 — Z는 보호, T는 투명.", + "contact_avatar_badge_hint": "배지는 주소 유형에 따라 자동으로 선택됩니다.", "contact_avatar_choose": "이미지 선택…", "contact_avatar_copy_failed": "그 이미지를 복사할 수 없습니다.", "contact_avatar_icon": "아이콘", "contact_avatar_image": "이미지", "contact_avatar_image_hint": "이미지는 앱에 복사되어 원본이 이동해도 계속 사용할 수 있습니다.", "contact_avatar_remove": "제거", + "contact_avatar_shielded": "보호", + "contact_avatar_transparent": "투명", "contact_global": "모든 지갑에 표시(전역 연락처)", "contact_global_badge_tt": "전역 연락처 — 모든 지갑에서 표시됨", "contact_global_tt": "켜짐: 어떤 지갑을 불러오든 이 연락처가 계속 표시됩니다. 꺼짐: 현재 지갑에만 속합니다.", diff --git a/res/lang/pt.json b/res/lang/pt.json index 3a0d575..d7e7ca5 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -289,13 +289,15 @@ "console_zoom_out": "Diminuir zoom", "contact_avatar": "AVATAR", "contact_avatar_badge": "Selo", - "contact_avatar_badge_hint": "Usa o selo de tipo de endereço — Z para blindado, T para transparente.", + "contact_avatar_badge_hint": "O selo é escolhido automaticamente conforme o tipo de endereço.", "contact_avatar_choose": "Escolher imagem…", "contact_avatar_copy_failed": "Não foi possível copiar essa imagem.", "contact_avatar_icon": "Ícone", "contact_avatar_image": "Imagem", "contact_avatar_image_hint": "A imagem é copiada para o app para continuar disponível se o original for movido.", "contact_avatar_remove": "Remover", + "contact_avatar_shielded": "Blindado", + "contact_avatar_transparent": "Transparente", "contact_global": "Mostrar em todas as carteiras (contato global)", "contact_global_badge_tt": "Contato global — visível em todas as carteiras", "contact_global_tt": "Ativado: este contato permanece visível em qualquer carteira que você carregar. Desativado: ele pertence apenas à carteira atual.", diff --git a/res/lang/ru.json b/res/lang/ru.json index 9d3fb7b..a0b412e 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -289,13 +289,15 @@ "console_zoom_out": "Уменьшить", "contact_avatar": "АВАТАР", "contact_avatar_badge": "Значок", - "contact_avatar_badge_hint": "Использует значок типа адреса — Z для защищённого, T для прозрачного.", + "contact_avatar_badge_hint": "Значок выбирается автоматически по типу адреса.", "contact_avatar_choose": "Выбрать изображение…", "contact_avatar_copy_failed": "Не удалось скопировать это изображение.", "contact_avatar_icon": "Иконка", "contact_avatar_image": "Изображение", "contact_avatar_image_hint": "Изображение копируется в приложение, чтобы оставаться доступным, если оригинал переместят.", "contact_avatar_remove": "Удалить", + "contact_avatar_shielded": "Защищённый", + "contact_avatar_transparent": "Прозрачный", "contact_global": "Показывать во всех кошельках (глобальный контакт)", "contact_global_badge_tt": "Глобальный контакт — виден во всех кошельках", "contact_global_tt": "Вкл.: этот контакт остаётся видимым, какой бы кошелёк вы ни загрузили. Выкл.: он принадлежит только текущему кошельку.", diff --git a/res/lang/zh.json b/res/lang/zh.json index f694969..d391e82 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -289,13 +289,15 @@ "console_zoom_out": "缩小", "contact_avatar": "头像", "contact_avatar_badge": "标记", - "contact_avatar_badge_hint": "使用地址类型标记——Z 表示隐蔽地址,T 表示透明地址。", + "contact_avatar_badge_hint": "标记会根据地址类型自动选择。", "contact_avatar_choose": "选择图片…", "contact_avatar_copy_failed": "无法复制该图片。", "contact_avatar_icon": "图标", "contact_avatar_image": "图片", "contact_avatar_image_hint": "图片会复制到应用中,即使原文件移动也能保持可用。", "contact_avatar_remove": "移除", + "contact_avatar_shielded": "隐蔽", + "contact_avatar_transparent": "透明", "contact_global": "在每个钱包中显示(全局联系人)", "contact_global_badge_tt": "全局联系人——在每个钱包中可见", "contact_global_tt": "开启:无论您加载哪个钱包,此联系人都保持可见。关闭:它仅属于当前钱包。", diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index fcf2d32..b1a9493 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -248,16 +248,30 @@ void RenderContactsTab(App* app) if (t) s += "\xE2\x80\xA6"; return s; }; + // Middle-truncate to fit maxW, keeping both ends — an address reads best head + tail + // (addresses are ASCII base58/bech32, so byte-wise trimming is safe). + auto fitMiddle = [&](std::string s, ImFont* f, float maxW) { + auto w = [&](const std::string& t){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, t.c_str()).x; }; + if (w(s) <= maxW || s.size() <= 8) return s; + const std::string ell = "\xE2\x80\xA6"; + size_t head = s.size() / 2, tail = s.size() - head; + while (head + tail > 6) { + std::string cand = s.substr(0, head) + ell + s.substr(s.size() - tail); + if (w(cand) <= maxW) return cand; + if (head >= tail) --head; else --tail; + } + return s.substr(0, 3) + ell + s.substr(s.size() - 3); + }; // ---- Live preview: the contact card as it will appear in the list -------------------- { ImDrawList* pdl = ImGui::GetWindowDrawList(); ImFont* nameF = material::Type().subtitle1(); ImFont* addrF = material::Type().caption(); - ImFont* letF = material::Type().subtitle2(); - ImFont* icoF = material::Type().iconSmall(); + ImFont* letF = material::Type().subtitle1(); + ImFont* icoF = material::Type().iconMed(); const float pad = Layout::spacingMd(); - const float avR = 20.0f * dp; + const float avR = 26.0f * dp; const float blockH = nameF->LegacySize + Layout::spacingXs() + addrF->LegacySize; const float ph = pad * 2.0f + std::max(avR * 2.0f, blockH); const float pw = ImGui::GetContentRegionAvail().x; @@ -282,8 +296,8 @@ void RenderContactsTab(App* app) pdl->AddText(nameF, nameF->LegacySize, ImVec2(tx, ty), hasName ? material::OnSurface() : material::OnSurfaceDisabled(), nm.c_str()); const bool hasAddr = s_edit_address[0] != '\0'; - std::string ad = fitText(hasAddr ? std::string(s_edit_address) : std::string(TR("contact_preview_addr")), - addrF, textRight - tx); + std::string ad = hasAddr ? fitMiddle(std::string(s_edit_address), addrF, textRight - tx) + : fitText(std::string(TR("contact_preview_addr")), addrF, textRight - tx); pdl->AddText(addrF, addrF->LegacySize, ImVec2(tx, ty + nameF->LegacySize + Layout::spacingXs()), hasAddr ? material::OnSurfaceMedium() : material::OnSurfaceDisabled(), ad.c_str()); ImGui::Dummy(ImVec2(pw, ph)); @@ -342,13 +356,43 @@ void RenderContactsTab(App* app) ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("contact_avatar")); { - const char* segs[3] = { TR("contact_avatar_badge"), TR("contact_avatar_icon"), - TR("contact_avatar_image") }; - float segH = 30.0f * dp; + // Custom icon + label segmented control (the shared helper is single-font; avatar modes + // read clearer with a glyph). Track + active pill mirror material::SegmentedControl. + const char* segLbl[3] = { TR("contact_avatar_badge"), TR("contact_avatar_icon"), + TR("contact_avatar_image") }; + const char* segIco[3] = { ICON_MD_BADGE, ICON_MD_PALETTE, ICON_MD_IMAGE }; + ImFont* segIcoF = material::Type().iconSmall(); + float segH = 34.0f * dp; float segW = ImGui::GetContentRegionAvail().x; + float cellW = segW / 3.0f; ImVec2 sMin = ImGui::GetCursorScreenPos(); - int clk = material::SegmentedControl(ImGui::GetWindowDrawList(), sMin, segW, segH, - segs, 3, s_edit_avatar_mode, btnFont, "##avseg", dp); + ImDrawList* sdl = ImGui::GetWindowDrawList(); + sdl->AddRectFilled(sMin, ImVec2(sMin.x + segW, sMin.y + segH), + material::WithAlpha(material::OnSurface(), 20), segH * 0.5f); + int clk = -1; + for (int i = 0; i < 3; i++) { + ImVec2 cMin(sMin.x + i * cellW, sMin.y), cMax(cMin.x + cellW, sMin.y + segH); + bool active = (s_edit_avatar_mode == i); + bool hov = ImGui::IsMouseHoveringRect(cMin, cMax); + if (active) + sdl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp), + ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp), + material::WithAlpha(material::Primary(), 210), (segH - 4.0f * dp) * 0.5f); + ImU32 fg = active ? IM_COL32(255, 255, 255, 255) : (hov ? material::OnSurface() : material::OnSurfaceMedium()); + float igW = segIcoF->CalcTextSizeA(segIcoF->LegacySize, FLT_MAX, 0, segIco[i]).x; + float lbW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, segLbl[i]).x; + float gapI = 5.0f * dp; + float startX = cMin.x + (cellW - (igW + gapI + lbW)) * 0.5f; + sdl->AddText(segIcoF, segIcoF->LegacySize, + ImVec2(startX, cMin.y + (segH - segIcoF->LegacySize) * 0.5f), fg, segIco[i]); + sdl->AddText(btnFont, btnFont->LegacySize, + ImVec2(startX + igW + gapI, cMin.y + (segH - btnFont->LegacySize) * 0.5f), fg, segLbl[i]); + ImGui::PushID(i); + ImGui::SetCursorScreenPos(cMin); + if (ImGui::InvisibleButton("##avseg", ImVec2(cellW, segH))) clk = i; + if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::PopID(); + } if (clk >= 0 && clk != s_edit_avatar_mode) { s_edit_avatar_mode = clk; // Switching mode resets the value unless it already matches that mode's prefix. @@ -363,16 +407,42 @@ void RenderContactsTab(App* app) // The mode content fills the rest of the avatar column (fixed height → no jump on switch). if (s_edit_avatar_mode == 0) { - // Badge: explanatory hint, centered in the remaining space below the segmented control. + // Badge: show the two example chips (Z shielded / T transparent) — the badge is picked + // automatically from the address type — plus a short caption. Centered in the space below. + ImDrawList* bdl = ImGui::GetWindowDrawList(); + ImFont* letF = material::Type().subtitle1(); + ImFont* capF = material::Type().caption(); + ImVec2 a = ImGui::GetContentRegionAvail(); + ImVec2 origin = ImGui::GetCursorScreenPos(); + const float chipR = 24.0f * dp; + const float chipGap = 40.0f * dp; + const float labelY = chipR * 2.0f + Layout::spacingXs() + capF->LegacySize; const char* msg = TR("contact_avatar_badge_hint"); - ImFont* f = material::Type().caption(); - ImVec2 a = ImGui::GetContentRegionAvail(); // remaining w,h from the current cursor - float wrapW = a.x - 16.0f * dp; - ImVec2 ts = f->CalcTextSizeA(f->LegacySize, wrapW, 0, msg); - float x0 = ImGui::GetCursorPosX(), y0 = ImGui::GetCursorPosY(); - ImGui::SetCursorPosX(x0 + std::max(0.0f, (a.x - ts.x) * 0.5f)); - ImGui::SetCursorPosY(y0 + std::max(0.0f, (a.y - ts.y) * 0.4f)); - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + std::min(ts.x, wrapW)); + float wrapW = a.x - 8.0f * dp; + ImVec2 ms = capF->CalcTextSizeA(capF->LegacySize, wrapW, 0, msg); + const float blockH = labelY + Layout::spacingMd() + ms.y; + float cy = origin.y + std::max(0.0f, (a.y - blockH) * 0.42f); + float cx = origin.x + a.x * 0.5f; + struct Chip { bool sh; const char* lbl; }; + Chip chips[2] = { { true, TR("contact_avatar_shielded") }, { false, TR("contact_avatar_transparent") } }; + for (int i = 0; i < 2; i++) { + float ccx = cx + (i == 0 ? -(chipR + chipGap * 0.5f) : (chipR + chipGap * 0.5f)); + ImVec2 cc(ccx, cy + chipR); + ImU32 col = contactTypeColor(chips[i].sh, light); + bdl->AddCircleFilled(cc, chipR, material::WithAlpha(col, light ? 45 : 60)); + bdl->AddCircle(cc, chipR, material::WithAlpha(col, 190), 0, 1.6f * dp); + const char* L = chips[i].sh ? "Z" : "T"; + ImVec2 ls = letF->CalcTextSizeA(letF->LegacySize, FLT_MAX, 0, L); + bdl->AddText(letF, letF->LegacySize, ImVec2(cc.x - ls.x * 0.5f, cc.y - ls.y * 0.5f), col, L); + ImVec2 lb = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, chips[i].lbl); + bdl->AddText(capF, capF->LegacySize, + ImVec2(cc.x - lb.x * 0.5f, cy + chipR * 2.0f + Layout::spacingXs()), + material::OnSurfaceMedium(), chips[i].lbl); + } + // Caption below the chips (wrapped, centered). + ImGui::SetCursorScreenPos(ImVec2(origin.x + std::max(0.0f, (a.x - std::min(ms.x, wrapW)) * 0.5f), + cy + labelY + Layout::spacingMd())); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + std::min(ms.x, wrapW)); material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceDisabled(), msg); ImGui::PopTextWrapPos(); } else if (s_edit_avatar_mode == 1) { @@ -424,28 +494,69 @@ void RenderContactsTab(App* app) ImGui::EndChild(); ImGui::PopStyleColor(); } else { - // Image: choose / remove a custom picture (copied into app data on pick). - if (s_edit_avatar.rfind("img:", 0) == 0) { - std::string path = s_edit_avatar.substr(4); - std::string fname = std::filesystem::path(path).filename().string(); - material::Type().textColored(material::TypeStyle::Body2, material::OnSurface(), - fitText(fname, material::Type().body2(), ImGui::GetContentRegionAvail().x).c_str()); - ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + // Image: a big circular preview (or a placeholder), the filename, and choose / remove. + const bool hasImg = s_edit_avatar.rfind("img:", 0) == 0; + ImDrawList* mdl = ImGui::GetWindowDrawList(); + ImFont* capF = material::Type().caption(); + const float prR = 44.0f * dp; + ImVec2 a = ImGui::GetContentRegionAvail(); + ImVec2 origin = ImGui::GetCursorScreenPos(); + float cx = origin.x + a.x * 0.5f; + float cy = origin.y + Layout::spacingSm() + prR; + ImVec2 cc(cx, cy); + if (hasImg) { + if (const AvatarTex* t = getAvatarTexture(s_edit_avatar.substr(4))) { + float u0=0,v0=0,u1=1,v1=1; + if (t->w > t->h) { float m=(t->w-t->h)*0.5f/t->w; u0=m; u1=1-m; } + else if (t->h > t->w) { float m=(t->h-t->w)*0.5f/t->h; v0=m; v1=1-m; } + mdl->AddImageRounded(t->tex, ImVec2(cc.x-prR, cc.y-prR), ImVec2(cc.x+prR, cc.y+prR), + ImVec2(u0,v0), ImVec2(u1,v1), IM_COL32_WHITE, prR); + mdl->AddCircle(cc, prR, material::WithAlpha(material::OnSurface(), 60), 0, 1.5f * dp); + } else { + // Image set but no longer loadable (moved/deleted) — show a broken-image glyph. + mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::ReadableError(), 35)); + mdl->AddCircle(cc, prR, material::WithAlpha(material::ReadableError(), 150), 0, 1.5f * dp); + ImFont* gf = material::Type().iconLarge(); + ImVec2 gs = gf->CalcTextSizeA(gf->LegacySize, FLT_MAX, 0, ICON_MD_BROKEN_IMAGE); + mdl->AddText(gf, gf->LegacySize, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f), + material::ReadableError(), ICON_MD_BROKEN_IMAGE); + } + } else { + mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::OnSurface(), 18)); + mdl->AddCircle(cc, prR, material::WithAlpha(material::OnSurface(), 55), 0, 1.5f * dp); + ImFont* gf = material::Type().iconLarge(); + ImVec2 gs = gf->CalcTextSizeA(gf->LegacySize, FLT_MAX, 0, ICON_MD_ADD_PHOTO_ALTERNATE); + mdl->AddText(gf, gf->LegacySize, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f), + material::OnSurfaceDisabled(), ICON_MD_ADD_PHOTO_ALTERNATE); } - if (material::TactileButton(TR("contact_avatar_choose"), ImVec2(0, 0), btnFont)) { + float rowY = cy + prR + Layout::spacingSm(); + if (hasImg) { + std::string fname = std::filesystem::path(s_edit_avatar.substr(4)).filename().string(); + std::string fit = fitText(fname, capF, a.x - 8.0f * dp); + ImVec2 fs = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, fit.c_str()); + mdl->AddText(capF, capF->LegacySize, ImVec2(cx - fs.x*0.5f, rowY), material::OnSurfaceMedium(), fit.c_str()); + rowY += capF->LegacySize + Layout::spacingSm(); + } + // Buttons row, centered. + ImGui::SetCursorScreenPos(ImVec2(origin.x, rowY)); + float chooseW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("contact_avatar_choose")).x + + ImGui::GetStyle().FramePadding.x * 2.0f + 20.0f * dp; + float removeW = hasImg ? (btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("contact_avatar_remove")).x + + ImGui::GetStyle().FramePadding.x * 2.0f + 20.0f * dp) : 0.0f; + float rowW = chooseW + (hasImg ? removeW + Layout::spacingSm() : 0.0f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (a.x - rowW) * 0.5f)); + if (material::TactileButton(TR("contact_avatar_choose"), ImVec2(chooseW, 0), btnFont)) { ImagePicker::open("", [](const std::string& src) { std::string dst = copyAvatarImage(src); if (!dst.empty()) { s_edit_avatar = "img:" + dst; s_edit_avatar_mode = 2; } else Notifications::instance().error(TR("contact_avatar_copy_failed")); }); } - if (s_edit_avatar.rfind("img:", 0) == 0) { - ImGui::SameLine(); - if (material::TactileButton(TR("contact_avatar_remove"), ImVec2(0, 0), btnFont)) + if (hasImg) { + ImGui::SameLine(0, Layout::spacingSm()); + if (material::TactileButton(TR("contact_avatar_remove"), ImVec2(removeW, 0), btnFont)) s_edit_avatar.clear(); } - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceDisabled(), TR("contact_avatar_image_hint")); } ImGui::EndChild(); // ##contactAvatarCol diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index c80ff12..0629d19 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1236,7 +1236,9 @@ void I18n::loadBuiltinEnglish() 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_badge_hint"] = "The badge is chosen automatically from the address type."; + strings_["contact_avatar_shielded"] = "Shielded"; + strings_["contact_avatar_transparent"] = "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.";