feat: sync thread grid during idle scaling, skip lock screen while pool mining, add paste preview to import key dialog

- Mining tab: sync s_selected_threads with actual thread count when idle
  thread scaling adjusts threads (solo via genproclimit, pool via
  threads_active), skipping sync during user drag
- Auto-lock: bypass lock screen overlay when xmrig pool mining is active
  so the mining UI remains accessible
- Import key dialog: add clipboard hover preview with transparent overlay
  on the input field, inline key type validation next to title (matching
  send tab paste button pattern), configurable via ui.toml
This commit is contained in:
dan_s
2026-03-19 06:10:46 -05:00
parent 9e94952e0a
commit 8645a82e4f
4 changed files with 119 additions and 44 deletions

View File

@@ -1168,6 +1168,8 @@ key-input = { height = 150 }
rescan-height-input = { width = 100 }
import-button = { width = 120, font = "button" }
close-button = { width = 100, font = "button" }
paste-preview-alpha = { size = 0.3 }
paste-preview-max-chars = { size = 200 }
[components]

View File

@@ -1051,7 +1051,10 @@ void App::render()
current_page_ != ui::NavPage::Settings);
bool daemonReady = state_.connected; // don't gate on sync state
if (state_.isLocked()) {
// Don't show lock screen while pool mining — xmrig runs independently
// of the wallet and locking would block the mining UI needlessly.
bool poolMiningActive = xmrig_manager_ && xmrig_manager_->isRunning();
if (state_.isLocked() && !poolMiningActive) {
// Lock screen — covers tab content just like the loading overlay
renderLockScreen();
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {

View File

@@ -33,6 +33,8 @@ static std::string s_status;
static int s_total_keys = 0;
static int s_imported_keys = 0;
static int s_failed_keys = 0;
static bool s_paste_previewing = false;
static std::string s_preview_text;
// Helper to detect key type
static std::string detectKeyType(const std::string& key)
@@ -95,6 +97,8 @@ void ImportKeyDialog::show()
s_total_keys = 0;
s_imported_keys = 0;
s_failed_keys = 0;
s_paste_previewing = false;
s_preview_text.clear();
}
bool ImportKeyDialog::isOpen()
@@ -134,6 +138,43 @@ void ImportKeyDialog::render(App* app)
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("import_key_tooltip"));
}
// Validation indicator inline with title — check preview text during hover,
// otherwise check the actual input
{
const char* checkSrc = s_key_input;
if (s_paste_previewing && !s_preview_text.empty())
checkSrc = s_preview_text.c_str();
if (checkSrc[0] != '\0') {
auto previewKeys = splitKeys(checkSrc);
int pz = 0, pt = 0, pu = 0;
for (const auto& k : previewKeys) {
std::string kt = detectKeyType(k);
if (kt == "z-spending") pz++;
else if (kt == "t-privkey") pt++;
else pu++;
}
if (pz > 0 || pt > 0) {
ImGui::SameLine();
char vbuf[128];
if (pz > 0 && pt > 0)
snprintf(vbuf, sizeof(vbuf), "%d shielded, %d transparent", pz, pt);
else if (pz > 0)
snprintf(vbuf, sizeof(vbuf), "%d shielded key(s)", pz);
else
snprintf(vbuf, sizeof(vbuf), "%d transparent key(s)", pt);
material::Type().textColored(material::TypeStyle::Caption, material::Success(), vbuf);
if (pu > 0) {
ImGui::SameLine();
snprintf(vbuf, sizeof(vbuf), "(%d unrecognized)", pu);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), vbuf);
}
} else if (pu > 0 && !s_paste_previewing) {
ImGui::SameLine();
material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format");
}
}
}
if (s_importing) {
ImGui::BeginDisabled();
@@ -142,62 +183,82 @@ void ImportKeyDialog::render(App* app)
ImGui::SetNextItemWidth(-1);
ImGui::InputTextMultiline("##KeyInput", s_key_input, sizeof(s_key_input),
ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput);
ImVec2 inputMin = ImGui::GetItemRectMin();
ImVec2 inputMax = ImGui::GetItemRectMax();
// Paste button
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
std::string trimmed(clipboard);
// Detect paste button hover before drawing it
ImVec2 pasteBtnPos = ImGui::GetCursorScreenPos();
// Estimate button size for hover detection
ImFont* btnFont = S.resolveFont(importBtn.font);
ImVec2 pasteBtnSize = btnFont
? ImVec2(btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("paste_from_clipboard")).x
+ ImGui::GetStyle().FramePadding.x * 2,
ImGui::GetFrameHeight())
: ImVec2(150, ImGui::GetFrameHeight());
bool paste_hovered = material::IsRectHovered(pasteBtnPos,
ImVec2(pasteBtnPos.x + pasteBtnSize.x, pasteBtnPos.y + pasteBtnSize.y));
// Handle preview state
if (paste_hovered && !s_paste_previewing) {
const char* clip = ImGui::GetClipboardText();
if (clip && clip[0] != '\0') {
std::string trimmed(clip);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str());
if (!trimmed.empty() && s_key_input[0] == '\0') {
s_preview_text = trimmed;
s_paste_previewing = true;
}
}
} else if (!paste_hovered && s_paste_previewing) {
s_paste_previewing = false;
s_preview_text.clear();
}
// Draw transparent preview text overlay on the input field
if (s_paste_previewing && !s_preview_text.empty()) {
ImVec2 textPos(inputMin.x + ImGui::GetStyle().FramePadding.x,
inputMin.y + ImGui::GetStyle().FramePadding.y);
ImVec4 previewCol = ImGui::GetStyleColorVec4(ImGuiCol_Text);
previewCol.w = S.drawElement("dialogs.import-key", "paste-preview-alpha").sizeOr(0.3f);
size_t maxChars = (size_t)S.drawElement("dialogs.import-key", "paste-preview-max-chars").sizeOr(200.0f);
// Clip to input rect
ImGui::GetWindowDrawList()->PushClipRect(inputMin, inputMax, true);
ImGui::GetWindowDrawList()->AddText(textPos, ImGui::ColorConvertFloat4ToU32(previewCol),
s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), maxChars));
ImGui::GetWindowDrawList()->PopClipRect();
}
// Paste button
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
if (s_paste_previewing) {
snprintf(s_key_input, sizeof(s_key_input), "%s", s_preview_text.c_str());
s_paste_previewing = false;
s_preview_text.clear();
} else {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
std::string trimmed(clipboard);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str());
}
}
}
ImGui::SameLine();
if (material::StyledButton(TR("clear"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
s_key_input[0] = '\0';
}
// Key validation indicator
if (s_key_input[0] != '\0') {
auto keys = splitKeys(s_key_input);
int zCount = 0, tCount = 0, unknownCount = 0;
for (const auto& key : keys) {
std::string kt = detectKeyType(key);
if (kt == "z-spending") zCount++;
else if (kt == "t-privkey") tCount++;
else unknownCount++;
}
if (zCount > 0 || tCount > 0) {
ImGui::PushFont(material::Type().iconSmall());
material::Type().textColored(material::TypeStyle::Caption, material::Success(), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, 2.0f);
char validBuf[128];
if (zCount > 0 && tCount > 0)
snprintf(validBuf, sizeof(validBuf), "%d shielded, %d transparent key(s)", zCount, tCount);
else if (zCount > 0)
snprintf(validBuf, sizeof(validBuf), "%d shielded key(s)", zCount);
else
snprintf(validBuf, sizeof(validBuf), "%d transparent key(s)", tCount);
material::Type().textColored(material::TypeStyle::Caption, material::Success(), validBuf);
if (unknownCount > 0) {
ImGui::SameLine();
snprintf(validBuf, sizeof(validBuf), "(%d unrecognized)", unknownCount);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), validBuf);
}
} else if (unknownCount > 0) {
ImGui::PushFont(material::Type().iconSmall());
material::Type().textColored(material::TypeStyle::Caption, material::Error(), ICON_MD_ERROR);
ImGui::PopFont();
ImGui::SameLine(0, 2.0f);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format");
}
s_paste_previewing = false;
s_preview_text.clear();
}
ImGui::Spacing();

View File

@@ -140,6 +140,15 @@ void RenderMiningTab(App* app)
s_threads_initialized = true;
}
// Sync thread grid with actual count when idle thread scaling adjusts threads
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active) {
if (s_pool_mode && state.pool_mining.xmrig_running && state.pool_mining.threads_active > 0) {
s_selected_threads = std::min(state.pool_mining.threads_active, max_threads);
} else if (mining.generate && mining.genproclimit > 0) {
s_selected_threads = std::min(mining.genproclimit, max_threads);
}
}
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = Layout::glassRounding();