feat(wallets): fixed max-height list + manage scanned folders

Two wallets-modal changes:

- The wallet list now always renders at its max height (kMaxVisibleRows) instead
  of shrinking to the wallet count, so the modal is a consistent size whether you
  have one wallet or many — fewer rows leave empty space, more than 7 scroll.

- Add a "Scanned folders" manager: each user-added scan folder is listed with a
  control to stop scanning it (removeExtraFolder + save + re-scan so its wallets
  drop out). Paths front-elide to keep the identifying leaf visible, with a
  full-path tooltip. The card-height math reserves the manager's rows so nothing
  clips. Two new i18n keys (wallets_scanned_folders / wallets_remove_folder),
  translated across all 8 languages; CJK subset rebuilt.

Verified on the sweep at 1.0 and 1.5x DPI: max-height list on both the few- and
many-wallet surfaces, the folder manager renders and stays clear of the footer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 07:04:43 -05:00
parent 09e0b962c3
commit 6310f51f65
12 changed files with 83 additions and 1 deletions

View File

@@ -71,7 +71,9 @@ public:
const int kMaxVisibleRows = 7;
ImFont* nameFont = Type().subtitle1();
ImFont* metaFont = Type().caption();
const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows);
// The list is always sized to its MAX height (kMaxVisibleRows) for a consistent modal size — it
// does not shrink to fit a few wallets; fewer rows leave empty space, more than 7 scroll.
const int visRows = kMaxVisibleRows;
const float cardPadY = Layout::spacingMd(); // roomier cards (was spacingSm)
const float walRowH = cardPadY * 2.0f + nameFont->LegacySize + Layout::spacingSm() + metaFont->LegacySize;
const float cardGap = Layout::spacingMd(); // more breathing room between wallet cards
@@ -81,9 +83,14 @@ public:
const float ctrlRow = ImGui::GetFrameHeightWithSpacing();
const float capRow = Type().caption()->LegacySize + style.ItemSpacing.y;
const int numScanFolders = app ? (int)app->walletIndex().extraFolders().size() : 0;
const float foldersH = numScanFolders > 0
? (capRow + (float)numScanFolders * ctrlRow + Layout::spacingXs()) // "Scanned folders:" + one row each
: 0.0f;
const float belowH = 4.0f * Layout::spacingSm()
+ (capRow + ctrlRow) // Create label + input row
+ ctrlRow // full-width "scan folder" button
+ foldersH // scanned-folders manager
+ (style.ItemSpacing.y + 1.0f) // separator
+ ctrlRow // footer buttons
+ 6.0f * style.ItemSpacing.y; // uncounted inter-item gaps
@@ -422,6 +429,51 @@ public:
});
}
// ---- Manage scanned folders: list each user-added folder with a control to stop scanning it.
// (The datadir is always scanned and isn't listed here — only the folders the user added.) -----
{
const auto& scanFolders = app->walletIndex().extraFolders();
if (!scanFolders.empty()) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_scanned_folders"));
ImFont* pf = Type().caption();
const float bh2 = ImGui::GetFrameHeight();
// Front-elide a long path so its identifying tail (the leaf folder) stays visible.
auto elideFront = [&](std::string s, float maxW) {
auto w = [&](const std::string& t){ return pf->CalcTextSizeA(pf->LegacySize, FLT_MAX, 0, t.c_str()).x; };
if (w(s) <= maxW) return s;
while (s.size() > 1 && w("\xE2\x80\xA6" + s) > maxW) {
s.erase(s.begin());
while (!s.empty() && (static_cast<unsigned char>(s.front()) & 0xC0) == 0x80) s.erase(s.begin());
}
return "\xE2\x80\xA6" + s;
};
std::string toRemove;
for (const auto& folder : scanFolders) {
ImGui::PushID(folder.c_str());
IconButtonStyle rm;
rm.color = OnSurfaceMedium();
rm.hoverColor = Error();
rm.hoverBg = StateHover();
rm.bgRounding = 4.0f * dp;
rm.tooltip = TR("wallets_remove_folder");
if (IconButton("##rmScanFolder", ICON_MD_CLOSE, Type().iconSmall(), ImVec2(bh2, bh2), rm))
toRemove = folder;
ImGui::SameLine(0, Layout::spacingSm());
ImGui::AlignTextToFramePadding();
const float textW = listW - bh2 - 2.0f * Layout::spacingSm();
Type().textColored(TypeStyle::Caption, OnSurface(), elideFront(folder, textW).c_str());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", folder.c_str());
ImGui::PopID();
}
if (!toRemove.empty()) {
app->walletIndex().removeExtraFolder(toRemove);
app->walletIndex().save();
s_needScan = true; // re-scan so wallets from the dropped folder disappear
}
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator();
// ---- Refresh: icon-only, de-emphasized next to the text actions -----------------------

View File

@@ -287,6 +287,8 @@ void I18n::loadBuiltinEnglish()
strings_["wallets_never"] = "Never opened";
strings_["wallets_external_tt"] = "Outside your data directory \xE2\x80\x94 Open links it in place (no copy).";
strings_["wallets_scan_folder"] = "Scan another folder for wallets\xE2\x80\xA6";
strings_["wallets_scanned_folders"] = "Scanned folders:";
strings_["wallets_remove_folder"] = "Stop scanning this folder";
strings_["wallets_folder_invalid"] = "That folder doesn't exist.";
strings_["wallets_new_label"] = "Create a new wallet:";
strings_["wallets_new_hint"] = "Name (e.g. savings)";