Compare commits
5 Commits
1a41dec8d8
...
03dde25339
| Author | SHA1 | Date | |
|---|---|---|---|
| 03dde25339 | |||
| 3fbb64d14c | |||
| 1b0006a4af | |||
| ba13875eaf | |||
| a5d44c8aca |
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "Adresse zum Buch hinzugefügt",
|
"address_book_added": "Adresse zum Buch hinzugefügt",
|
||||||
"address_book_confirm_delete": "Löschen bestätigen?",
|
"address_book_confirm_delete": "Löschen bestätigen?",
|
||||||
"address_book_count": "%zu Adressen gespeichert",
|
"address_book_count": "%zu Adressen gespeichert",
|
||||||
|
"address_book_count_one": "%zu Adresse gespeichert",
|
||||||
"address_book_deleted": "Eintrag gelöscht",
|
"address_book_deleted": "Eintrag gelöscht",
|
||||||
"address_book_edit": "Adresse bearbeiten",
|
"address_book_edit": "Adresse bearbeiten",
|
||||||
"address_book_empty": "Keine gespeicherten Adressen. Klicken Sie auf 'Neue hinzufügen', um eine hinzuzufügen.",
|
"address_book_empty": "Keine gespeicherten Adressen. Klicken Sie auf 'Neue hinzufügen', um eine hinzuzufügen.",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "Dirección agregada a la libreta",
|
"address_book_added": "Dirección agregada a la libreta",
|
||||||
"address_book_confirm_delete": "¿Confirmar eliminación?",
|
"address_book_confirm_delete": "¿Confirmar eliminación?",
|
||||||
"address_book_count": "%zu direcciones guardadas",
|
"address_book_count": "%zu direcciones guardadas",
|
||||||
|
"address_book_count_one": "%zu dirección guardada",
|
||||||
"address_book_deleted": "Entrada eliminada",
|
"address_book_deleted": "Entrada eliminada",
|
||||||
"address_book_edit": "Editar Dirección",
|
"address_book_edit": "Editar Dirección",
|
||||||
"address_book_empty": "No hay direcciones guardadas. Haz clic en 'Agregar Nueva' para añadir una.",
|
"address_book_empty": "No hay direcciones guardadas. Haz clic en 'Agregar Nueva' para añadir una.",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "Adresse ajoutée au carnet",
|
"address_book_added": "Adresse ajoutée au carnet",
|
||||||
"address_book_confirm_delete": "Confirmer la suppression ?",
|
"address_book_confirm_delete": "Confirmer la suppression ?",
|
||||||
"address_book_count": "%zu adresses enregistrées",
|
"address_book_count": "%zu adresses enregistrées",
|
||||||
|
"address_book_count_one": "%zu adresse enregistrée",
|
||||||
"address_book_deleted": "Entrée supprimée",
|
"address_book_deleted": "Entrée supprimée",
|
||||||
"address_book_edit": "Modifier l'adresse",
|
"address_book_edit": "Modifier l'adresse",
|
||||||
"address_book_empty": "Aucune adresse enregistrée. Cliquez sur 'Ajouter' pour en créer une.",
|
"address_book_empty": "Aucune adresse enregistrée. Cliquez sur 'Ajouter' pour en créer une.",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "アドレスをアドレス帳に追加しました",
|
"address_book_added": "アドレスをアドレス帳に追加しました",
|
||||||
"address_book_confirm_delete": "削除しますか?",
|
"address_book_confirm_delete": "削除しますか?",
|
||||||
"address_book_count": "%zu 件のアドレスを保存済み",
|
"address_book_count": "%zu 件のアドレスを保存済み",
|
||||||
|
"address_book_count_one": "%zu 件のアドレスを保存",
|
||||||
"address_book_deleted": "エントリを削除しました",
|
"address_book_deleted": "エントリを削除しました",
|
||||||
"address_book_edit": "アドレスを編集",
|
"address_book_edit": "アドレスを編集",
|
||||||
"address_book_empty": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。",
|
"address_book_empty": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "주소록에 주소를 추가했습니다",
|
"address_book_added": "주소록에 주소를 추가했습니다",
|
||||||
"address_book_confirm_delete": "삭제하시겠습니까?",
|
"address_book_confirm_delete": "삭제하시겠습니까?",
|
||||||
"address_book_count": "저장된 주소 %zu개",
|
"address_book_count": "저장된 주소 %zu개",
|
||||||
|
"address_book_count_one": "주소 %zu개 저장됨",
|
||||||
"address_book_deleted": "항목이 삭제되었습니다",
|
"address_book_deleted": "항목이 삭제되었습니다",
|
||||||
"address_book_edit": "주소 편집",
|
"address_book_edit": "주소 편집",
|
||||||
"address_book_empty": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.",
|
"address_book_empty": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "Endereço adicionado ao livro",
|
"address_book_added": "Endereço adicionado ao livro",
|
||||||
"address_book_confirm_delete": "Confirmar exclusão?",
|
"address_book_confirm_delete": "Confirmar exclusão?",
|
||||||
"address_book_count": "%zu endereços salvos",
|
"address_book_count": "%zu endereços salvos",
|
||||||
|
"address_book_count_one": "%zu endereço salvo",
|
||||||
"address_book_deleted": "Entrada excluída",
|
"address_book_deleted": "Entrada excluída",
|
||||||
"address_book_edit": "Editar Endereço",
|
"address_book_edit": "Editar Endereço",
|
||||||
"address_book_empty": "Nenhum endereço salvo. Clique em 'Adicionar Novo' para criar um.",
|
"address_book_empty": "Nenhum endereço salvo. Clique em 'Adicionar Novo' para criar um.",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "Адрес добавлен в книгу",
|
"address_book_added": "Адрес добавлен в книгу",
|
||||||
"address_book_confirm_delete": "Подтвердить удаление?",
|
"address_book_confirm_delete": "Подтвердить удаление?",
|
||||||
"address_book_count": "%zu адресов сохранено",
|
"address_book_count": "%zu адресов сохранено",
|
||||||
|
"address_book_count_one": "Сохранён %zu адрес",
|
||||||
"address_book_deleted": "Запись удалена",
|
"address_book_deleted": "Запись удалена",
|
||||||
"address_book_edit": "Редактировать адрес",
|
"address_book_edit": "Редактировать адрес",
|
||||||
"address_book_empty": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.",
|
"address_book_empty": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"address_book_added": "地址已添加到通讯录",
|
"address_book_added": "地址已添加到通讯录",
|
||||||
"address_book_confirm_delete": "确认删除?",
|
"address_book_confirm_delete": "确认删除?",
|
||||||
"address_book_count": "已保存 %zu 个地址",
|
"address_book_count": "已保存 %zu 个地址",
|
||||||
|
"address_book_count_one": "已保存 %zu 个地址",
|
||||||
"address_book_deleted": "条目已删除",
|
"address_book_deleted": "条目已删除",
|
||||||
"address_book_edit": "编辑地址",
|
"address_book_edit": "编辑地址",
|
||||||
"address_book_empty": "没有保存的地址。点击'添加新地址'创建一个。",
|
"address_book_empty": "没有保存的地址。点击'添加新地址'创建一个。",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
|
||||||
#include "config/settings.h"
|
#include "config/settings.h"
|
||||||
|
#include "data/address_book.h"
|
||||||
#include "ui/schema/skin_manager.h"
|
#include "ui/schema/skin_manager.h"
|
||||||
#include "ui/notifications.h"
|
#include "ui/notifications.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
@@ -90,6 +91,29 @@ const char* kDemoMnemonic =
|
|||||||
const char* kDemoZAddr =
|
const char* kDemoZAddr =
|
||||||
"zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
|
"zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
|
||||||
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";
|
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";
|
||||||
|
|
||||||
|
// Contacts-view sweep: seed a few demo contacts (Z + T types, some global) so the Cards/List/Table
|
||||||
|
// modes render with data, and restore the real book afterward. Uses sweepSetEntries (no disk write),
|
||||||
|
// so the user's persisted address book is never touched even if the sweep is interrupted.
|
||||||
|
std::vector<data::AddressBookEntry> s_contactsSweepBackup;
|
||||||
|
void seedSweepContacts(App& a)
|
||||||
|
{
|
||||||
|
s_contactsSweepBackup = a.addressBook().entries();
|
||||||
|
data::AddressBookEntry e1("drgx pool payout address", kDemoZAddr, "mining pool payouts"); // Z, global
|
||||||
|
data::AddressBookEntry e2("exchange deposit", "t1DemoTransparentAddressForUiSweep00000", ""); // T
|
||||||
|
// Scope to the active wallet so it's visible (the tab hides contacts not in the active wallet);
|
||||||
|
// non-empty hash also keeps it non-global -> no globe badge, so the list shows a global/non-global mix.
|
||||||
|
e2.scope = a.activeWalletIdentityHash();
|
||||||
|
data::AddressBookEntry e3("cold savings",
|
||||||
|
"zs1sweepdemocoldsavingsaddressforuicapture0000000000000000000000000000", "long-term storage"); // Z, global
|
||||||
|
a.addressBook().sweepSetEntries({ e1, e2, e3 });
|
||||||
|
}
|
||||||
|
void restoreSweepContacts(App& a)
|
||||||
|
{
|
||||||
|
a.addressBook().sweepSetEntries(s_contactsSweepBackup);
|
||||||
|
s_contactsSweepBackup.clear();
|
||||||
|
if (a.settings()) a.settings()->setContactsViewMode(0);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::string App::screenshotDir() const
|
std::string App::screenshotDir() const
|
||||||
@@ -373,6 +397,16 @@ void App::buildSweepCatalog()
|
|||||||
add("modal-key-export", ui::NavPage::Overview,
|
add("modal-key-export", ui::NavPage::Overview,
|
||||||
[](App&) { ui::KeyExportDialog::show(kDemoZAddr, ui::KeyExportDialog::KeyType::Private); },
|
[](App&) { ui::KeyExportDialog::show(kDemoZAddr, ui::KeyExportDialog::KeyType::Private); },
|
||||||
[](App&) { ui::KeyExportDialog::hide(); });
|
[](App&) { ui::KeyExportDialog::hide(); });
|
||||||
|
// Contacts address-list view modes, each seeded with demo contacts (restored after; no disk write).
|
||||||
|
add("contacts-cards", ui::NavPage::Contacts,
|
||||||
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(0); },
|
||||||
|
[](App& a) { restoreSweepContacts(a); });
|
||||||
|
add("contacts-list", ui::NavPage::Contacts,
|
||||||
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1); },
|
||||||
|
[](App& a) { restoreSweepContacts(a); });
|
||||||
|
add("contacts-table", ui::NavPage::Contacts,
|
||||||
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(2); },
|
||||||
|
[](App& a) { restoreSweepContacts(a); });
|
||||||
add("modal-about", ui::NavPage::Overview,
|
add("modal-about", ui::NavPage::Overview,
|
||||||
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
||||||
add("modal-settings", ui::NavPage::Settings,
|
add("modal-settings", ui::NavPage::Settings,
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ bool Settings::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
loadScalar(j, "portfolio_style", portfolio_style_);
|
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||||
|
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||||
|
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
||||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||||
loadScalar(j, "console_text_color", console_text_color_);
|
loadScalar(j, "console_text_color", console_text_color_);
|
||||||
@@ -428,6 +430,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["window_opacity"] = window_opacity_;
|
j["window_opacity"] = window_opacity_;
|
||||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||||
j["portfolio_style"] = portfolio_style_;
|
j["portfolio_style"] = portfolio_style_;
|
||||||
|
j["contacts_view_mode"] = contacts_view_mode_;
|
||||||
j["scanline_enabled"] = scanline_enabled_;
|
j["scanline_enabled"] = scanline_enabled_;
|
||||||
j["console_line_accents"] = console_line_accents_;
|
j["console_line_accents"] = console_line_accents_;
|
||||||
j["console_text_color"] = console_text_color_;
|
j["console_text_color"] = console_text_color_;
|
||||||
|
|||||||
@@ -184,6 +184,9 @@ public:
|
|||||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||||
int getPortfolioStyle() const { return portfolio_style_; }
|
int getPortfolioStyle() const { return portfolio_style_; }
|
||||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
|
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||||
|
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||||
|
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
|
|
||||||
// Console scanline effect
|
// Console scanline effect
|
||||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||||
@@ -490,6 +493,7 @@ private:
|
|||||||
#endif
|
#endif
|
||||||
std::string balance_layout_ = "classic";
|
std::string balance_layout_ = "classic";
|
||||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||||
|
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
||||||
bool scanline_enabled_ = true;
|
bool scanline_enabled_ = true;
|
||||||
bool console_line_accents_ = true; // left color accent bars in console output
|
bool console_line_accents_ = true; // left color accent bars in console output
|
||||||
bool console_text_color_ = true; // per-channel text coloring in console output
|
bool console_text_color_ = true; // per-channel text coloring in console output
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ public:
|
|||||||
*/
|
*/
|
||||||
size_t size() const { return entries_.size(); }
|
size_t size() const { return entries_.size(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief UI-sweep ONLY: replace the in-memory entries WITHOUT persisting to disk, so the sweep can
|
||||||
|
* seed demo contacts and restore the real book without a disk write. Do not use outside the sweep.
|
||||||
|
*/
|
||||||
|
void sweepSetEntries(std::vector<AddressBookEntry> e) { entries_ = std::move(e); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if empty
|
* @brief Check if empty
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
|
|
||||||
#include "contacts_tab.h"
|
#include "contacts_tab.h"
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
#include "../../data/address_book.h"
|
#include "../../data/address_book.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../layout.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -215,8 +218,37 @@ void RenderContactsTab(App* app)
|
|||||||
ImGui::BeginChild("##ContactsScroll", avail, false,
|
ImGui::BeginChild("##ContactsScroll", avail, false,
|
||||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||||
|
|
||||||
// Toolbar
|
// Toolbar — icon + label actions; the primary "Add New" is accented so it stands out.
|
||||||
if (material::TactileButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
const float dp = ui::Layout::dpiScale();
|
||||||
|
ImFont* tbTxtF = S.resolveFont(actionBtn.font);
|
||||||
|
ImFont* tbIcoF = material::Type().iconSmall();
|
||||||
|
// Renders a tactile button with a leading Material icon (icon font) + label (text font). ImGui has no
|
||||||
|
// native two-font button, so draw the styled rect via TactileButton (empty ##id label) then paint the
|
||||||
|
// icon + label over it. `primary` accents the main action; the icon/label inherit the (disabled-aware)
|
||||||
|
// text alpha so they gray out correctly inside BeginDisabled().
|
||||||
|
auto toolbarBtn = [&](const char* id, const char* glyph, const char* label, bool primary) -> bool {
|
||||||
|
ImGui::PushFont(tbIcoF); const float iw = ImGui::CalcTextSize(glyph).x; ImGui::PopFont();
|
||||||
|
ImGui::PushFont(tbTxtF); const float tw = ImGui::CalcTextSize(label).x; ImGui::PopFont();
|
||||||
|
const float gap = 6.0f * dp;
|
||||||
|
const float w = ImGui::GetStyle().FramePadding.x * 2.0f + iw + gap + tw;
|
||||||
|
const float h = ImGui::GetFrameHeight();
|
||||||
|
if (primary) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||||
|
}
|
||||||
|
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||||
|
const bool clicked = material::TactileButton(id, ImVec2(w, h), tbTxtF);
|
||||||
|
if (primary) ImGui::PopStyleColor(3);
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
const ImU32 col = ImGui::GetColorU32(ImGuiCol_Text);
|
||||||
|
const float sx = p.x + (w - (iw + gap + tw)) * 0.5f;
|
||||||
|
dl->AddText(tbIcoF, tbIcoF->LegacySize, ImVec2(sx, p.y + (h - tbIcoF->LegacySize) * 0.5f), col, glyph);
|
||||||
|
dl->AddText(tbTxtF, tbTxtF->LegacySize, ImVec2(sx + iw + gap, p.y + (h - tbTxtF->LegacySize) * 0.5f), col, label);
|
||||||
|
return clicked;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (toolbarBtn("##ab_add", ICON_MD_PERSON_ADD, TR("address_book_add_new"), /*primary=*/true)) {
|
||||||
s_show_add_dialog = true;
|
s_show_add_dialog = true;
|
||||||
s_focus_edit_field = true;
|
s_focus_edit_field = true;
|
||||||
clearEditFields();
|
clearEditFields();
|
||||||
@@ -226,7 +258,7 @@ void RenderContactsTab(App* app)
|
|||||||
|
|
||||||
if (!has_selection) ImGui::BeginDisabled();
|
if (!has_selection) ImGui::BeginDisabled();
|
||||||
|
|
||||||
if (material::TactileButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font)))
|
if (toolbarBtn("##ab_edit", ICON_MD_EDIT, TR("edit"), false))
|
||||||
openEdit();
|
openEdit();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -234,16 +266,36 @@ void RenderContactsTab(App* app)
|
|||||||
// Delete — relabel to a visible confirm prompt while armed (not just a toast).
|
// Delete — relabel to a visible confirm prompt while armed (not just a toast).
|
||||||
bool armed = has_selection && s_confirm_delete_idx == s_selected_index;
|
bool armed = has_selection && s_confirm_delete_idx == s_selected_index;
|
||||||
const char* delLabel = armed ? TR("address_book_confirm_delete") : TR("delete");
|
const char* delLabel = armed ? TR("address_book_confirm_delete") : TR("delete");
|
||||||
if (material::TactileButton(delLabel, ImVec2(0,0), S.resolveFont(actionBtn.font)))
|
if (toolbarBtn("##ab_del", ICON_MD_DELETE, delLabel, false))
|
||||||
doDelete();
|
doDelete();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
if (material::TactileButton(TR("copy_address"), ImVec2(0,0), S.resolveFont(actionBtn.font)))
|
if (toolbarBtn("##ab_copy", ICON_MD_CONTENT_COPY, TR("copy_address"), false))
|
||||||
doCopy();
|
doCopy();
|
||||||
|
|
||||||
if (!has_selection) ImGui::EndDisabled();
|
if (!has_selection) ImGui::EndDisabled();
|
||||||
|
|
||||||
|
// Address-list view toggle (Cards / List / Table) — right-aligned on the toolbar row; persisted.
|
||||||
|
int viewMode = app->settings() ? app->settings()->getContactsViewMode() : 0;
|
||||||
|
{
|
||||||
|
const char* segIcons[3] = { ICON_MD_VIEW_AGENDA, ICON_MD_VIEW_LIST, ICON_MD_TABLE_ROWS };
|
||||||
|
const float segH = ImGui::GetFrameHeight();
|
||||||
|
const float segW = 44.0f * dp * 3.0f;
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - segW));
|
||||||
|
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||||
|
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
||||||
|
segIcons, 3, viewMode, material::Type().iconSmall(),
|
||||||
|
"##contactsView", dp);
|
||||||
|
if (segClk >= 0 && app->settings()) {
|
||||||
|
app->settings()->setContactsViewMode(segClk);
|
||||||
|
app->settings()->save();
|
||||||
|
viewMode = segClk;
|
||||||
|
}
|
||||||
|
ImGui::Dummy(ImVec2(segW, segH));
|
||||||
|
}
|
||||||
|
|
||||||
// Search / filter
|
// Search / filter
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||||
@@ -268,31 +320,50 @@ void RenderContactsTab(App* app)
|
|||||||
visibleRows.push_back(i);
|
visibleRows.push_back(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address list — size the table to fill the tab, leaving room for the count footer.
|
// Address list — fill the tab, leaving room for the count footer.
|
||||||
float footerH = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
const float footerH = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
||||||
float tableH = ImGui::GetContentRegionAvail().y - footerH;
|
float listH = ImGui::GetContentRegionAvail().y - footerH;
|
||||||
if (tableH < 120.0f) tableH = 120.0f;
|
if (listH < 120.0f * dp) listH = 120.0f * dp;
|
||||||
|
|
||||||
|
const bool lightTheme = material::IsLightTheme();
|
||||||
|
auto typeColor = [&](bool shielded) -> ImU32 {
|
||||||
|
ImVec4 c = shielded ? (lightTheme ? ImVec4(0.10f,0.55f,0.38f,1.0f) : ImVec4(0.35f,0.80f,0.60f,1.0f))
|
||||||
|
: (lightTheme ? ImVec4(0.72f,0.48f,0.05f,1.0f) : ImVec4(0.95f,0.72f,0.30f,1.0f));
|
||||||
|
return ImGui::ColorConvertFloat4ToU32(c);
|
||||||
|
};
|
||||||
|
// Centered Material empty state (no contacts yet / no search match).
|
||||||
|
auto emptyState = [&](const char* iconGlyph, const char* msgKey) {
|
||||||
|
const char* msg = TR(msgKey);
|
||||||
|
ImFont* icoF = material::Type().iconLarge();
|
||||||
|
ImFont* txtF = material::Type().body2();
|
||||||
|
const float availW = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImVec2 is = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, iconGlyph);
|
||||||
|
ImVec2 ts = txtF->CalcTextSizeA(txtF->LegacySize, FLT_MAX, 0, msg);
|
||||||
|
ImGui::SetCursorPos(ImVec2((availW - is.x) * 0.5f, ImGui::GetCursorPosY() + listH * 0.32f));
|
||||||
|
ImGui::PushFont(icoF);
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceDisabled()), "%s", iconGlyph);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SetCursorPosX((availW - ts.x) * 0.5f);
|
||||||
|
material::Type().textColored(material::TypeStyle::Body2, material::OnSurfaceMedium(), msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (viewMode == 2) {
|
||||||
|
// ── TABLE mode (material-ized): no outer/grid borders, row backgrounds, interactive sort. ──
|
||||||
if (ImGui::BeginTable("AddressBookTable", 3,
|
if (ImGui::BeginTable("AddressBookTable", 3,
|
||||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable |
|
ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable |
|
||||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY,
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, listH)))
|
||||||
ImVec2(0, tableH)))
|
|
||||||
{
|
{
|
||||||
float labelColW = (addrTable.columns.count("label") && addrTable.columns.at("label").width > 0) ? addrTable.columns.at("label").width : 150;
|
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultSort, 1.5f);
|
||||||
float notesColW = (addrTable.columns.count("notes") && addrTable.columns.at("notes").width > 0) ? addrTable.columns.at("notes").width : 150;
|
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch, 2.6f);
|
||||||
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_DefaultSort, labelColW);
|
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
||||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch);
|
|
||||||
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthFixed, notesColW);
|
|
||||||
ImGui::TableSetupScrollFreeze(0, 1);
|
ImGui::TableSetupScrollFreeze(0, 1);
|
||||||
ImGui::TableHeadersRow();
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
// Sort the visible view by the active column (indices stay storage indices).
|
|
||||||
if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) {
|
if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) {
|
||||||
if (specs->SpecsCount > 0) {
|
if (specs->SpecsCount > 0) {
|
||||||
const ImGuiTableColumnSortSpecs& sc = specs->Specs[0];
|
const ImGuiTableColumnSortSpecs& sc = specs->Specs[0];
|
||||||
bool asc = sc.SortDirection != ImGuiSortDirection_Descending;
|
bool asc = sc.SortDirection != ImGuiSortDirection_Descending;
|
||||||
const auto& entries = book.entries();
|
const auto& entries = book.entries();
|
||||||
std::stable_sort(visibleRows.begin(), visibleRows.end(),
|
std::stable_sort(visibleRows.begin(), visibleRows.end(), [&](size_t a, size_t b) {
|
||||||
[&](size_t a, size_t b) {
|
|
||||||
const std::string* ka; const std::string* kb;
|
const std::string* ka; const std::string* kb;
|
||||||
switch (sc.ColumnIndex) {
|
switch (sc.ColumnIndex) {
|
||||||
case 1: ka = &entries[a].address; kb = &entries[b].address; break;
|
case 1: ka = &entries[a].address; kb = &entries[b].address; break;
|
||||||
@@ -304,73 +375,137 @@ void RenderContactsTab(App* app)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (book.empty()) {
|
|
||||||
ImGui::TableNextRow();
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::TextDisabled("%s", TR("address_book_empty"));
|
|
||||||
} else if (visibleRows.empty()) {
|
|
||||||
ImGui::TableNextRow();
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::TextDisabled("%s", TR("contacts_search_no_match"));
|
|
||||||
} else {
|
|
||||||
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
||||||
size_t i = visibleRows[vi];
|
size_t i = visibleRows[vi];
|
||||||
const auto& entry = book.entries()[i];
|
const auto& entry = book.entries()[i];
|
||||||
|
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::PushID(static_cast<int>(i));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
bool is_selected = (s_selected_index == static_cast<int>(i));
|
bool is_selected = (s_selected_index == static_cast<int>(i));
|
||||||
if (ImGui::Selectable(entry.label.c_str(), is_selected,
|
if (ImGui::Selectable(entry.label.c_str(), is_selected,
|
||||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||||
if (s_selected_index != static_cast<int>(i)) s_confirm_delete_idx = -1;
|
if (s_selected_index != static_cast<int>(i)) s_confirm_delete_idx = -1;
|
||||||
s_selected_index = static_cast<int>(i);
|
s_selected_index = static_cast<int>(i);
|
||||||
|
if (ImGui::IsMouseDoubleClicked(0)) openEdit();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
|
||||||
s_selected_index = static_cast<int>(i);
|
|
||||||
openEdit();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
// Z/T type badge + address in normal (legible) text — not muted.
|
|
||||||
// Darker/more-saturated variants on light skins so the badge doesn't wash out.
|
|
||||||
bool shielded = isShieldedAddr(entry.address);
|
bool shielded = isShieldedAddr(entry.address);
|
||||||
bool lightTheme = material::IsLightTheme();
|
ImGui::PushFont(material::Type().subtitle2());
|
||||||
ImVec4 zCol = lightTheme ? ImVec4(0.10f, 0.55f, 0.38f, 1.0f) : ImVec4(0.35f, 0.80f, 0.60f, 1.0f);
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(typeColor(shielded)), "%s", shielded ? "Z" : "T");
|
||||||
ImVec4 tCol = lightTheme ? ImVec4(0.72f, 0.48f, 0.05f, 1.0f) : ImVec4(0.95f, 0.72f, 0.30f, 1.0f);
|
ImGui::PopFont();
|
||||||
ImGui::TextColored(shielded ? zCol : tCol, "%s", shielded ? "Z" : "T");
|
ImGui::SameLine(0.0f, 6.0f * dp);
|
||||||
ImGui::SameLine(0.0f, 6.0f);
|
std::string addr_display = util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||||
std::string addr_display = entry.address;
|
|
||||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 40;
|
|
||||||
if (addr_display.length() > static_cast<size_t>(addrTruncLen)) {
|
|
||||||
addr_display = util::truncateMiddle(addr_display, addrFrontLbl.truncate, addrBackLbl.truncate);
|
|
||||||
}
|
|
||||||
ImGui::TextUnformatted(addr_display.c_str());
|
ImGui::TextUnformatted(addr_display.c_str());
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", entry.address.c_str());
|
||||||
material::Tooltip("%s", entry.address.c_str());
|
|
||||||
}
|
|
||||||
// Globe badge marks a global contact (shown in every wallet).
|
|
||||||
if (entry.isGlobal()) {
|
if (entry.isGlobal()) {
|
||||||
ImGui::SameLine(0.0f, 8.0f);
|
ImGui::SameLine(0.0f, 8.0f * dp);
|
||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()),
|
ImGui::PushFont(material::Type().iconSmall());
|
||||||
"%s", ICON_MD_PUBLIC);
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()), "%s", ICON_MD_PUBLIC);
|
||||||
|
ImGui::PopFont();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_badge_tt"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_badge_tt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::TextUnformatted(entry.notes.c_str());
|
ImGui::TextUnformatted(entry.notes.c_str());
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// ── CARDS (0) / LIST (1) mode — tactile Material items, no grid lines. ──
|
||||||
|
const bool asCard = (viewMode == 0);
|
||||||
|
// Cards/list have no interactive sort header — sort by label ascending.
|
||||||
|
{
|
||||||
|
const auto& entries = book.entries();
|
||||||
|
std::stable_sort(visibleRows.begin(), visibleRows.end(), [&](size_t a, size_t b) {
|
||||||
|
return toLower(entries[a].label).compare(toLower(entries[b].label)) < 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0,0,0,0));
|
||||||
|
ImGui::BeginChild("##contactList", ImVec2(0, listH), false, ImGuiWindowFlags_NoBackground);
|
||||||
|
if (visibleRows.empty()) {
|
||||||
|
emptyState(ICON_MD_CONTACTS, book.empty() ? "address_book_empty" : "contacts_search_no_match");
|
||||||
|
} else {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0,0,0,0));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0,0,0,0));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, asCard ? 8.0f * dp : 0.0f));
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
ImFont* lblF = material::Type().subtitle2();
|
||||||
|
ImFont* adrF = material::Type().caption();
|
||||||
|
ImFont* icoF = material::Type().iconSmall();
|
||||||
|
const float pad = 12.0f * dp;
|
||||||
|
const float avR = 15.0f * dp;
|
||||||
|
const float rowH = avR * 2.0f + (asCard ? 16.0f * dp : 12.0f * dp);
|
||||||
|
const float round = 10.0f * dp;
|
||||||
|
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
||||||
|
size_t i = visibleRows[vi];
|
||||||
|
const auto& entry = book.entries()[i];
|
||||||
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
const bool selected = (s_selected_index == static_cast<int>(i));
|
||||||
|
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||||
|
const float w = ImGui::GetContentRegionAvail().x;
|
||||||
|
if (ImGui::Selectable("##it", selected,
|
||||||
|
ImGuiSelectableFlags_SpanAvailWidth | ImGuiSelectableFlags_AllowDoubleClick,
|
||||||
|
ImVec2(0, rowH))) {
|
||||||
|
if (!selected) s_confirm_delete_idx = -1;
|
||||||
|
s_selected_index = static_cast<int>(i);
|
||||||
|
if (ImGui::IsMouseDoubleClicked(0)) openEdit();
|
||||||
|
}
|
||||||
|
const bool hov = ImGui::IsItemHovered();
|
||||||
|
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
const ImVec2 mn = p, mx(p.x + w, p.y + rowH);
|
||||||
|
if (asCard) {
|
||||||
|
ImU32 fill = selected ? material::WithAlpha(material::Primary(), 42)
|
||||||
|
: hov ? material::WithAlpha(material::OnSurface(), 26)
|
||||||
|
: material::WithAlpha(material::OnSurface(), 12);
|
||||||
|
dl->AddRectFilled(mn, mx, fill, round);
|
||||||
|
if (selected) dl->AddRect(mn, mx, material::WithAlpha(material::Primary(), 150), round, 0, 1.6f * dp);
|
||||||
|
} else {
|
||||||
|
if (selected) dl->AddRectFilled(mn, mx, material::WithAlpha(material::Primary(), 34), 0);
|
||||||
|
else if (hov) dl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 16), 0);
|
||||||
|
dl->AddLine(ImVec2(mn.x + pad, mx.y - 0.5f), ImVec2(mx.x - pad, mx.y - 0.5f),
|
||||||
|
material::WithAlpha(material::OnSurface(), 24), 1.0f);
|
||||||
|
}
|
||||||
|
// Circular type avatar with the Z/T letter.
|
||||||
|
const bool shielded = isShieldedAddr(entry.address);
|
||||||
|
const ImU32 tu = typeColor(shielded);
|
||||||
|
const ImVec2 avC(mn.x + pad + avR, mn.y + rowH * 0.5f);
|
||||||
|
dl->AddCircleFilled(avC, avR, material::WithAlpha(tu, lightTheme ? 45 : 60));
|
||||||
|
dl->AddCircle(avC, avR, material::WithAlpha(tu, 190), 0, 1.4f * dp);
|
||||||
|
const char* letter = shielded ? "Z" : "T";
|
||||||
|
const ImVec2 ls = lblF->CalcTextSizeA(lblF->LegacySize, FLT_MAX, 0, letter);
|
||||||
|
dl->AddText(lblF, lblF->LegacySize, ImVec2(avC.x - ls.x * 0.5f, avC.y - ls.y * 0.5f), tu, letter);
|
||||||
|
// Label (line 1) + muted address (line 2).
|
||||||
|
const float tx = mn.x + pad + avR * 2.0f + pad;
|
||||||
|
const float trailW = entry.isGlobal() ? 28.0f * dp : 0.0f;
|
||||||
|
const float textMaxX = mx.x - pad - trailW;
|
||||||
|
const float blockH = lblF->LegacySize + adrF->LegacySize + 3.0f * dp;
|
||||||
|
const float ty = mn.y + rowH * 0.5f - blockH * 0.5f;
|
||||||
|
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||||
|
dl->AddText(lblF, lblF->LegacySize, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||||
|
dl->PopClipRect();
|
||||||
|
std::string addr = util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||||
|
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
|
||||||
|
material::OnSurfaceMedium(), addr.c_str());
|
||||||
|
// Trailing globe badge for global contacts.
|
||||||
|
if (entry.isGlobal()) {
|
||||||
|
const ImVec2 gs = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, ICON_MD_PUBLIC);
|
||||||
|
dl->AddText(icoF, icoF->LegacySize,
|
||||||
|
ImVec2(mx.x - pad - gs.x, mn.y + rowH * 0.5f - gs.y * 0.5f),
|
||||||
|
material::OnSurfaceMedium(), ICON_MD_PUBLIC);
|
||||||
|
}
|
||||||
|
if (hov) material::Tooltip("%s", entry.address.c_str());
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
ImGui::PopStyleColor(3);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
// Status line
|
// Status line — singular form for exactly one contact ("1 address saved", not "1 addresses saved").
|
||||||
ImGui::TextDisabled(TR("address_book_count"), book.size());
|
ImGui::TextDisabled(TR(book.size() == 1 ? "address_book_count_one" : "address_book_count"), book.size());
|
||||||
|
|
||||||
// Keyboard shortcuts — only when the tab owns focus (no field being edited, no modal up).
|
// Keyboard shortcuts — only when the tab owns focus (no field being edited, no modal up).
|
||||||
if (!searchActive && !ImGui::IsAnyItemActive() &&
|
if (!searchActive && !ImGui::IsAnyItemActive() &&
|
||||||
|
|||||||
@@ -85,9 +85,20 @@ void KeyExportDialog::render(App* app)
|
|||||||
(void)keyDisplay;
|
(void)keyDisplay;
|
||||||
(void)copyBtn;
|
(void)copyBtn;
|
||||||
|
|
||||||
// Modal scales to 85% of the window width. BeginOverlayDialog multiplies the width by
|
// Size the card to the address field's own natural width — the widest deterministic element before
|
||||||
// Layout::dpiScale(); divide it out here so the final card is exactly 85% at any font scale.
|
// the key is revealed — so the field fills the card and sits under its left-aligned "Address:" label
|
||||||
const float cardW = (0.85f * ImGui::GetMainViewport()->Size.x) / Layout::dpiScale();
|
// instead of floating centered in an over-wide card. This adapts to the address type (z vs t) and the
|
||||||
|
// font scale automatically (no magic width). Mirrors AddressCopyField's boxW math; the revealed
|
||||||
|
// key+QR layout adapts to whatever width this yields (it stacks the QR when narrow). Bounded to 85%
|
||||||
|
// of the window as an upper safety limit. BeginOverlayDialog re-applies dpiScale(), so divide it out.
|
||||||
|
const std::string chunkedAddr = widgets::ChunkString(s_address, 4);
|
||||||
|
const float addrPadX = ImGui::GetStyle().FramePadding.x + 4.0f; // matches AddressCopyField
|
||||||
|
const float addrBoxW = ImGui::CalcTextSize(chunkedAddr.c_str()).x + addrPadX * 2.0f + 2.0f;
|
||||||
|
// The BlurFloat overlay card insets content by 28px per side (draw_helpers BeginOverlayDialog); add
|
||||||
|
// that (+ a little slack) so the address box fits on one line under its label instead of wrapping.
|
||||||
|
const float wantW = addrBoxW + 28.0f * 2.0f + 12.0f;
|
||||||
|
const float maxW = 0.85f * ImGui::GetMainViewport()->Size.x;
|
||||||
|
const float cardW = (wantW < maxW ? wantW : maxW) / Layout::dpiScale();
|
||||||
material::OverlayDialogSpec ov;
|
material::OverlayDialogSpec ov;
|
||||||
ov.title = title; ov.p_open = &s_open;
|
ov.title = title; ov.p_open = &s_open;
|
||||||
ov.style = material::OverlayStyle::BlurFloat;
|
ov.style = material::OverlayStyle::BlurFloat;
|
||||||
|
|||||||
@@ -64,9 +64,18 @@ void QRPopupDialog::render(App* app)
|
|||||||
auto qr = S.drawElement("dialogs.qr-popup", "qr-code");
|
auto qr = S.drawElement("dialogs.qr-popup", "qr-code");
|
||||||
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
|
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
|
||||||
|
|
||||||
// Match the key-export modal: 85% of the window width (divide out the dpiScale that
|
// Size the card to the address's own natural (chunked) box width — the widest content element — so
|
||||||
// BeginOverlayDialog re-applies, so the final card is exactly 85% at any font scale).
|
// AddressCopyField fills the card under its label instead of floating centered in an over-wide card
|
||||||
const float cardW = (0.85f * ImGui::GetMainViewport()->Size.x) / Layout::dpiScale();
|
// (matches the key-export dialog). Adapts to address type + font scale; bounded to 85% of the window.
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
const std::string chunkedAddr = widgets::ChunkString(s_address, 4);
|
||||||
|
const float addrPadX = ImGui::GetStyle().FramePadding.x + 4.0f; // matches AddressCopyField
|
||||||
|
const float addrBoxW = ImGui::CalcTextSize(chunkedAddr.c_str()).x + addrPadX * 2.0f + 2.0f;
|
||||||
|
// The BlurFloat overlay card insets content by 28px per side (draw_helpers BeginOverlayDialog); add
|
||||||
|
// that (+ a little slack) so the address box fits on one line under its label instead of wrapping.
|
||||||
|
const float wantW = addrBoxW + 28.0f * 2.0f + 12.0f;
|
||||||
|
const float maxW = 0.85f * ImGui::GetMainViewport()->Size.x;
|
||||||
|
const float cardW = (wantW < maxW ? wantW : maxW) / dp;
|
||||||
material::OverlayDialogSpec ov;
|
material::OverlayDialogSpec ov;
|
||||||
ov.title = TR("qr_title"); ov.p_open = &s_open;
|
ov.title = TR("qr_title"); ov.p_open = &s_open;
|
||||||
ov.style = material::OverlayStyle::BlurFloat;
|
ov.style = material::OverlayStyle::BlurFloat;
|
||||||
@@ -83,10 +92,10 @@ void QRPopupDialog::render(App* app)
|
|||||||
|
|
||||||
// Center the QR code (responsive — larger, to suit the wider modal).
|
// Center the QR code (responsive — larger, to suit the wider modal).
|
||||||
float window_width = ImGui::GetWindowWidth();
|
float window_width = ImGui::GetWindowWidth();
|
||||||
float qr_size = qr.size > 0 ? (float)qr.size : 280.0f;
|
float qr_size = (qr.size > 0 ? (float)qr.size : 280.0f) * dp; // schema/fallback are logical px
|
||||||
const float responsive = window_width * 0.5f;
|
const float responsive = window_width * 0.5f; // window_width is already dp-scaled
|
||||||
if (responsive > qr_size) qr_size = responsive;
|
if (responsive > qr_size) qr_size = responsive;
|
||||||
if (qr_size > 420.0f) qr_size = 420.0f;
|
if (qr_size > 420.0f * dp) qr_size = 420.0f * dp;
|
||||||
float padding = (window_width - qr_size) / 2.0f;
|
float padding = (window_width - qr_size) / 2.0f;
|
||||||
if (padding < 0.0f) padding = 0.0f;
|
if (padding < 0.0f) padding = 0.0f;
|
||||||
|
|
||||||
|
|||||||
@@ -1225,6 +1225,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["contact_global_badge_tt"] = "Global contact — visible in every wallet";
|
strings_["contact_global_badge_tt"] = "Global contact — visible in every wallet";
|
||||||
strings_["address_book_added"] = "Address added to book";
|
strings_["address_book_added"] = "Address added to book";
|
||||||
strings_["address_book_count"] = "%zu addresses saved";
|
strings_["address_book_count"] = "%zu addresses saved";
|
||||||
|
strings_["address_book_count_one"] = "%zu address saved";
|
||||||
strings_["address_book_deleted"] = "Entry deleted";
|
strings_["address_book_deleted"] = "Entry deleted";
|
||||||
strings_["address_book_edit"] = "Edit Address";
|
strings_["address_book_edit"] = "Edit Address";
|
||||||
strings_["address_book_empty"] = "No saved addresses. Click 'Add New' to add one.";
|
strings_["address_book_empty"] = "No saved addresses. Click 'Add New' to add one.";
|
||||||
|
|||||||
Reference in New Issue
Block a user