diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 0ff306e..b906a83 100644 Binary files a/res/fonts/NotoSansCJK-Subset.ttf and b/res/fonts/NotoSansCJK-Subset.ttf differ diff --git a/res/lang/de.json b/res/lang/de.json index 2b1feb6..0a34b4c 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -1322,6 +1322,7 @@ "wallets_active": "Aktiv", "wallets_add": "Ordner hinzufügen", "wallets_add_folder": "Auch einen weiteren Ordner nach Wallet-Dateien durchsuchen:", + "wallets_add_folder_toggle": "+ Weiteren Ordner nach Wallets durchsuchen…", "wallets_button": "Wallets…", "wallets_col_addresses": "Adressen", "wallets_col_balance": "Guthaben", @@ -1332,6 +1333,7 @@ "wallets_creating": "Wallet wird erstellt — der Node wird neu gestartet…", "wallets_current": "aktuell", "wallets_exists": "Eine Wallet mit diesem Namen existiert bereits.", + "wallets_external_tt": "Außerhalb deines Datenverzeichnisses — mit Import hineinkopieren.", "wallets_folder_hint": "/pfad/zum/ordner mit .dat-Wallet-Dateien", "wallets_folder_invalid": "Dieser Ordner existiert nicht.", "wallets_import": "Importieren", diff --git a/res/lang/es.json b/res/lang/es.json index fe49dfe..93454db 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -1322,6 +1322,7 @@ "wallets_active": "Activa", "wallets_add": "Agregar carpeta", "wallets_add_folder": "Escanear también otra carpeta en busca de archivos de cartera:", + "wallets_add_folder_toggle": "+ Buscar wallets en otra carpeta…", "wallets_button": "Carteras…", "wallets_col_addresses": "Direcciones", "wallets_col_balance": "Saldo", @@ -1332,6 +1333,7 @@ "wallets_creating": "Creando cartera — el nodo se reiniciará…", "wallets_current": "actual", "wallets_exists": "Ya existe una cartera con ese nombre.", + "wallets_external_tt": "Fuera de tu directorio de datos — usa Importar para copiarlo.", "wallets_folder_hint": "/ruta/a/carpeta con archivos .dat de cartera", "wallets_folder_invalid": "Esa carpeta no existe.", "wallets_import": "Importar", diff --git a/res/lang/fr.json b/res/lang/fr.json index 6ac7aed..23c605b 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -1322,6 +1322,7 @@ "wallets_active": "Actif", "wallets_add": "Ajouter le dossier", "wallets_add_folder": "Analyser aussi un autre dossier à la recherche de fichiers de portefeuille :", + "wallets_add_folder_toggle": "+ Analyser un autre dossier pour les portefeuilles…", "wallets_button": "Portefeuilles…", "wallets_col_addresses": "Adresses", "wallets_col_balance": "Solde", @@ -1332,6 +1333,7 @@ "wallets_creating": "Création du portefeuille — le nœud va redémarrer…", "wallets_current": "actuel", "wallets_exists": "Un portefeuille portant ce nom existe déjà.", + "wallets_external_tt": "Hors de votre répertoire de données — Importer pour le copier.", "wallets_folder_hint": "/chemin/vers/le/dossier contenant les fichiers .dat", "wallets_folder_invalid": "Ce dossier n'existe pas.", "wallets_import": "Importer", diff --git a/res/lang/ja.json b/res/lang/ja.json index 1e4c18a..ecf0f9a 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -1322,6 +1322,7 @@ "wallets_active": "アクティブ", "wallets_add": "フォルダを追加", "wallets_add_folder": "ウォレットファイルを検索する別のフォルダを追加:", + "wallets_add_folder_toggle": "+ 別のフォルダーをウォレット検索…", "wallets_button": "ウォレット…", "wallets_col_addresses": "アドレス", "wallets_col_balance": "残高", @@ -1332,6 +1333,7 @@ "wallets_creating": "ウォレットを作成中 — ノードが再起動します…", "wallets_current": "現在", "wallets_exists": "その名前のウォレットは既に存在します。", + "wallets_external_tt": "データディレクトリの外にあります — インポートでコピーします。", "wallets_folder_hint": "wallet .dat ファイルのあるフォルダのパス", "wallets_folder_invalid": "そのフォルダは存在しません。", "wallets_import": "インポート", diff --git a/res/lang/ko.json b/res/lang/ko.json index 63825b7..59347bb 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -1322,6 +1322,7 @@ "wallets_active": "활성", "wallets_add": "폴더 추가", "wallets_add_folder": "지갑 파일을 검색할 폴더를 추가로 지정:", + "wallets_add_folder_toggle": "+ 다른 폴더에서 지갑 검색…", "wallets_button": "지갑…", "wallets_col_addresses": "주소", "wallets_col_balance": "잔액", @@ -1332,6 +1333,7 @@ "wallets_creating": "지갑을 만드는 중 — 노드가 재시작됩니다…", "wallets_current": "현재", "wallets_exists": "같은 이름의 지갑이 이미 존재합니다.", + "wallets_external_tt": "데이터 디렉터리 외부에 있음 — 가져오기로 복사합니다.", "wallets_folder_hint": "wallet-*.dat 파일이 있는 폴더 경로", "wallets_folder_invalid": "해당 폴더가 존재하지 않습니다.", "wallets_import": "가져오기", diff --git a/res/lang/pt.json b/res/lang/pt.json index 7d95b2a..99d565a 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -1322,6 +1322,7 @@ "wallets_active": "Ativa", "wallets_add": "Adicionar pasta", "wallets_add_folder": "Verificar também outra pasta em busca de arquivos de carteira:", + "wallets_add_folder_toggle": "+ Procurar carteiras noutra pasta…", "wallets_button": "Carteiras…", "wallets_col_addresses": "Endereços", "wallets_col_balance": "Saldo", @@ -1332,6 +1333,7 @@ "wallets_creating": "Criando carteira — o nó será reiniciado…", "wallets_current": "atual", "wallets_exists": "Já existe uma carteira com esse nome.", + "wallets_external_tt": "Fora do seu diretório de dados — use Importar para copiá-lo.", "wallets_folder_hint": "/caminho/para/pasta com arquivos .dat de carteira", "wallets_folder_invalid": "Essa pasta não existe.", "wallets_import": "Importar", diff --git a/res/lang/ru.json b/res/lang/ru.json index e8a0cae..e4c613c 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -1322,6 +1322,7 @@ "wallets_active": "Активен", "wallets_add": "Добавить папку", "wallets_add_folder": "Также сканировать другую папку на наличие файлов кошельков:", + "wallets_add_folder_toggle": "+ Искать кошельки в другой папке…", "wallets_button": "Кошельки…", "wallets_col_addresses": "Адреса", "wallets_col_balance": "Баланс", @@ -1332,6 +1333,7 @@ "wallets_creating": "Создание кошелька — узел будет перезапущен…", "wallets_current": "текущий", "wallets_exists": "Кошелёк с таким именем уже существует.", + "wallets_external_tt": "Вне вашего каталога данных — «Импорт» скопирует его.", "wallets_folder_hint": "/путь/к/папке с файлами кошельков .dat", "wallets_folder_invalid": "Такой папки не существует.", "wallets_import": "Импорт", diff --git a/res/lang/zh.json b/res/lang/zh.json index f24a0d8..e2cdc94 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -1322,6 +1322,7 @@ "wallets_active": "活跃", "wallets_add": "添加文件夹", "wallets_add_folder": "同时扫描另一个文件夹中的钱包文件:", + "wallets_add_folder_toggle": "+ 扫描其他文件夹中的钱包…", "wallets_button": "钱包…", "wallets_col_addresses": "地址", "wallets_col_balance": "余额", @@ -1332,6 +1333,7 @@ "wallets_creating": "正在创建钱包——节点将重启…", "wallets_current": "当前", "wallets_exists": "已存在同名钱包。", + "wallets_external_tt": "位于数据目录之外 — 导入以将其复制进来。", "wallets_folder_hint": "/含 .dat 钱包文件的文件夹路径", "wallets_folder_invalid": "该文件夹不存在。", "wallets_import": "导入", diff --git a/src/ui/windows/wallets_dialog.h b/src/ui/windows/wallets_dialog.h index 6456686..2a7dff6 100644 --- a/src/ui/windows/wallets_dialog.h +++ b/src/ui/windows/wallets_dialog.h @@ -48,20 +48,39 @@ public: using namespace material; App* app = s_app; const float dp = Layout::dpiScale(); + ImGuiStyle& style = ImGui::GetStyle(); if (s_needScan) { scan(); s_needScan = false; } - // FIXED cardHeight (not the auto-height overload): the table below sizes itself from - // GetContentRegionAvail().y, which is only stable when the dialog's content child is - // fixed-height. Auto-height makes that read self-referential and it diverges across a - // monitor move (dpiScale jump), sliding the create/footer rows off-screen. See Manage - // Portfolio (market_tab.cpp) for the same fixed-height pattern. + // ---- #1 Content-sized card (bounded) ----------------------------------------------------- + // Size the card to the wallet count (up to kMaxVisibleRows, then the list scrolls) so a short + // list doesn't float in a sea of empty space. Computed from the KNOWN row count, so it stays a + // fixed-height dialog (no auto-height feedback — that slid content off-screen on a monitor move). + const int kMaxVisibleRows = 8; + const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows); + const float tableRowH = ImGui::GetFrameHeight() + style.CellPadding.y * 2.0f; + const float tableHdrH = ImGui::GetTextLineHeight() + style.CellPadding.y * 2.0f; + const float tableH = tableHdrH + visRows * tableRowH + 4.0f * dp; + + const float ctrlRow = ImGui::GetFrameHeightWithSpacing(); + const float capRow = Type().caption()->LegacySize + style.ItemSpacing.y; + const float belowH = 3.0f * Layout::spacingSm() + + (capRow + ctrlRow) // Create label + input row + + (s_showAddFolder ? (capRow + ctrlRow) : capRow) // folder row OR toggle link + + (style.ItemSpacing.y + 1.0f) // separator + + ctrlRow // footer buttons + + 6.0f * style.ItemSpacing.y; // uncounted inter-item gaps + const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title + + Type().caption()->LegacySize + Layout::spacingSm(); // intro + gap + const float padV = 48.0f; // content-child padding (top+bottom) + margin + const float cardH = (headH + tableH + belowH + padV) / dp; + OverlayDialogSpec ov; ov.title = TR("wallets_title"); ov.p_open = &s_open; ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 780.0f; - ov.cardHeight = 620.0f; + ov.cardHeight = cardH; ov.idSuffix = "wallets"; if (BeginOverlayDialog(ov)) { Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_intro")); @@ -69,36 +88,52 @@ public: const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat"; - // Reserve room below the table for the create-wallet + add-folder rows (each a label + - // an input row), the separator, and the footer buttons — computed from real widget - // metrics so nothing clips at any DPI (a fixed px reserve was too small at 150%). - const float rowH = ImGui::GetFrameHeightWithSpacing(); - const float lblH = Type().caption()->LegacySize + ImGui::GetStyle().ItemSpacing.y; - const float belowH = 3.0f * Layout::spacingSm() + 2.0f * lblH + 3.0f * rowH + Layout::spacingMd(); - float tableH = ImGui::GetContentRegionAvail().y - belowH; - if (tableH < 120.0f * dp) tableH = 120.0f * dp; - + // ---- #2 Framed wallet list ------------------------------------------------------------ const ImGuiTableFlags tflags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | - ImGuiTableFlags_BordersInnerH; + ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_PadOuterX; if (ImGui::BeginTable("##wallets", 6, tflags, ImVec2(0, tableH))) { ImGui::TableSetupColumn(TR("wallets_col_name"), ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn(TR("wallets_col_size"), ImGuiTableColumnFlags_WidthFixed, 90.0f * dp); - ImGui::TableSetupColumn(TR("wallets_col_addresses"), ImGuiTableColumnFlags_WidthFixed, 80.0f * dp); + ImGui::TableSetupColumn(TR("wallets_col_addresses"), ImGuiTableColumnFlags_WidthFixed, 90.0f * dp); ImGui::TableSetupColumn(TR("wallets_col_balance"), ImGuiTableColumnFlags_WidthFixed, 130.0f * dp); ImGui::TableSetupColumn(TR("wallets_col_opened"), ImGuiTableColumnFlags_WidthFixed, 120.0f * dp); - ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 110.0f * dp); + ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 100.0f * dp); ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableHeadersRow(); + // ---- #3 Right-aligned headers for the numeric columns (Size / Addresses / Balance). + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int c = 0; c < 6; ++c) { + ImGui::TableSetColumnIndex(c); + const char* cn = ImGui::TableGetColumnName(c); + if (c == 5 || !cn || !cn[0]) continue; + if (c >= 1 && c <= 3) { + float w = ImGui::CalcTextSize(cn).x; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - w); + ImGui::TextUnformatted(cn); + } else { + ImGui::TableHeader(cn); + } + } + + auto rcell = [&](const std::string& s, bool muted) { + float w = ImGui::CalcTextSize(s.c_str()).x; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - w); + if (muted) ImGui::TextDisabled("%s", s.c_str()); else ImGui::TextUnformatted(s.c_str()); + }; + const char* kDash = "\xE2\x80\x94"; + + char nb[32]; for (std::size_t i = 0; i < s_rows.size(); ++i) { const WalletRow& r = s_rows[i]; const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName); const bool isCurrent = r.inDatadir && r.fileName == active; ImGui::TableNextRow(); + if (isCurrent) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, WithAlpha(Success(), 26)); ImGui::PushID(static_cast(i)); - // Name — + "current" badge, or a folder marker for out-of-datadir files. + // Name (+ #4 current badge / #6 external-file info icon) ImGui::TableNextColumn(); ImGui::TextUnformatted(r.fileName.c_str()); if (isCurrent) { @@ -106,38 +141,45 @@ public: ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Success()), "%s", TR("wallets_current")); } else if (!r.inDatadir) { ImGui::SameLine(0, 8.0f * dp); - ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", ICON_MD_FOLDER); - if (ImGui::IsItemHovered()) Tooltip("%s", r.dir.c_str()); + ImGui::PushFont(Type().iconSmall()); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", ICON_MD_INFO); + ImGui::PopFont(); + if (ImGui::IsItemHovered()) Tooltip("%s\n%s", TR("wallets_external_tt"), r.dir.c_str()); } ImGui::TableNextColumn(); - ImGui::TextUnformatted(util::Platform::formatFileSize(static_cast(r.sizeBytes)).c_str()); + rcell(util::Platform::formatFileSize(static_cast(r.sizeBytes)), false); ImGui::TableNextColumn(); - if (meta && meta->cachedAddressCount >= 0) ImGui::Text("%lld", meta->cachedAddressCount); - else ImGui::TextDisabled("—"); + if (meta && meta->cachedAddressCount >= 0) { snprintf(nb, sizeof(nb), "%lld", (long long)meta->cachedAddressCount); rcell(nb, false); } + else rcell(kDash, true); ImGui::TableNextColumn(); - if (meta && meta->cachedBalance >= 0.0) ImGui::Text("%.4f", meta->cachedBalance); - else ImGui::TextDisabled("—"); + if (meta && meta->cachedBalance >= 0.0) { snprintf(nb, sizeof(nb), "%.4f", meta->cachedBalance); rcell(nb, false); } + else rcell(kDash, true); ImGui::TableNextColumn(); if (meta && meta->lastOpenedEpoch > 0) ImGui::TextUnformatted(util::formatTimeAgo(meta->lastOpenedEpoch).c_str()); else ImGui::TextDisabled("%s", TR("wallets_never")); - // Action + // ---- #4 Action column (Active as a chip; Open/Import as buttons) --------------- ImGui::TableNextColumn(); if (isCurrent) { - ImGui::TextDisabled("%s", TR("wallets_active")); + const char* al = TR("wallets_active"); + ImVec2 ts = ImGui::CalcTextSize(al); + ImVec2 p = ImGui::GetCursorScreenPos(); + float cx = 8.0f * dp, cy = 3.0f * dp; + ImDrawList* cdl = ImGui::GetWindowDrawList(); + cdl->AddRectFilled(p, ImVec2(p.x + ts.x + cx * 2, p.y + ts.y + cy * 2), WithAlpha(Success(), 34), 4.0f * dp); + cdl->AddText(ImVec2(p.x + cx, p.y + cy), Success(), al); + ImGui::Dummy(ImVec2(ts.x + cx * 2, ts.y + cy * 2)); } else if (r.inDatadir) { if (StyledButton(TR("wallets_open"), ImVec2(90.0f * dp, 0))) { app->switchToWallet(r.fileName); - s_open = false; // node restarts; the main UI shows the reconnect overlay + s_open = false; } } else { - // Out-of-datadir wallets can't be loaded directly (the daemon rejects paths). - // Import = copy into the datadir under a wallet-*.dat name, then switch to it. if (StyledButton(TR("wallets_import"), ImVec2(90.0f * dp, 0))) importAndOpen(app, r); if (ImGui::IsItemHovered()) Tooltip("%s", TR("wallets_import_tt")); @@ -149,7 +191,7 @@ public: ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - // Create a fresh named wallet — the daemon mints it (with a seed phrase) on first load. + // ---- #5 Create a new wallet (primary) ------------------------------------------------- Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_new_label")); ImGui::SetNextItemWidth(-150.0f * dp); ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName)); @@ -164,33 +206,51 @@ public: } else { s_newName[0] = '\0'; Notifications::instance().info(TR("wallets_creating")); - app->switchToWallet(name); // daemon creates it on start (-usemnemonic=1 -> seed-backed) + app->switchToWallet(name); s_open = false; } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - // Add a folder to also scan for wallet files (no native picker — a validated path input). - Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_add_folder")); - ImGui::SetNextItemWidth(-150.0f * dp); - ImGui::InputTextWithHint("##walletFolder", TR("wallets_folder_hint"), s_newFolder, sizeof(s_newFolder)); - ImGui::SameLine(); - if (StyledButton(TR("wallets_add"), ImVec2(120.0f * dp, 0))) { - std::error_code ec; - std::string dir = s_newFolder; - if (!dir.empty() && std::filesystem::is_directory(dir, ec)) { - if (app->walletIndex().addExtraFolder(dir)) app->walletIndex().save(); - s_newFolder[0] = '\0'; - s_needScan = true; - } else { - Notifications::instance().warning(TR("wallets_folder_invalid")); + // ---- #5 Add-folder tucked behind a toggle so it doesn't compete with Create ----------- + if (!s_showAddFolder) { + ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium()); + ImGui::TextUnformatted(TR("wallets_add_folder_toggle")); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (ImGui::IsItemClicked()) s_showAddFolder = true; + } else { + Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_add_folder")); + ImGui::SetNextItemWidth(-150.0f * dp); + ImGui::InputTextWithHint("##walletFolder", TR("wallets_folder_hint"), s_newFolder, sizeof(s_newFolder)); + ImGui::SameLine(); + if (StyledButton(TR("wallets_add"), ImVec2(120.0f * dp, 0))) { + std::error_code ec; + std::string dir = s_newFolder; + if (!dir.empty() && std::filesystem::is_directory(dir, ec)) { + if (app->walletIndex().addExtraFolder(dir)) app->walletIndex().save(); + s_newFolder[0] = '\0'; + s_needScan = true; + } else { + Notifications::instance().warning(TR("wallets_folder_invalid")); + } } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Separator(); - if (StyledButton(TR("refresh"), ImVec2(110.0f * dp, 0))) s_needScan = true; + // ---- #6 Refresh: icon-only, de-emphasized next to the text actions -------------------- + { + float bh = ImGui::GetFrameHeight(); + IconButtonStyle rst; + rst.color = OnSurfaceMedium(); + rst.hoverBg = StateHover(); + rst.bgRounding = 4.0f * dp; + rst.tooltip = TR("refresh"); + if (IconButton("##walletsRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(bh, bh), rst)) + s_needScan = true; + } ImGui::SameLine(); if (StyledButton(TR("wallets_reveal"), ImVec2(150.0f * dp, 0))) util::Platform::openFolder(util::Platform::getDragonXDataDir()); @@ -279,6 +339,7 @@ private: static inline bool s_needScan = false; static inline char s_newFolder[512] = ""; static inline char s_newName[128] = ""; + static inline bool s_showAddFolder = false; static inline std::vector s_rows; }; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 1de7d20..369e60f 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -267,6 +267,8 @@ void I18n::loadBuiltinEnglish() strings_["wallets_import_hint"] = "Import to open"; strings_["wallets_import_tt"] = "This wallet is outside the data directory. Opening it will copy it in first (coming soon)."; strings_["wallets_add_folder"] = "Also scan another folder for wallet files:"; + strings_["wallets_add_folder_toggle"] = "+ Scan another folder for wallets\xE2\x80\xA6"; + strings_["wallets_external_tt"] = "Outside your data directory \xE2\x80\x94 Import to copy it in."; strings_["wallets_folder_hint"] = "/path/to/folder with wallet .dat files"; strings_["wallets_add"] = "Add folder"; strings_["wallets_folder_invalid"] = "That folder doesn't exist.";