Compare commits
6 Commits
dd9bc0bd2b
...
061db16004
| Author | SHA1 | Date | |
|---|---|---|---|
| 061db16004 | |||
| ec48538881 | |||
| 658c0f355b | |||
| 41f5548593 | |||
| 1dbbd44759 | |||
| b0cc6bcef4 |
91
src/app.cpp
91
src/app.cpp
@@ -822,6 +822,11 @@ void App::update()
|
|||||||
ps.algo = xs.algo;
|
ps.algo = xs.algo;
|
||||||
ps.version = xs.version;
|
ps.version = xs.version;
|
||||||
ps.connected = xs.connected;
|
ps.connected = xs.connected;
|
||||||
|
// Pool mining has a connect delay — announce once it's actually connected/hashing.
|
||||||
|
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
|
||||||
|
pool_starting_.store(false, std::memory_order_relaxed);
|
||||||
|
ui::Notifications::instance().success("Pool miner connected and hashing.");
|
||||||
|
}
|
||||||
// Get memory directly from OS (more reliable than API)
|
// Get memory directly from OS (more reliable than API)
|
||||||
double memMB = xmrig_manager_->getMemoryUsageMB();
|
double memMB = xmrig_manager_->getMemoryUsageMB();
|
||||||
ps.memory_used = static_cast<int64_t>(memMB * 1024.0 * 1024.0);
|
ps.memory_used = static_cast<int64_t>(memMB * 1024.0 * 1024.0);
|
||||||
@@ -836,6 +841,7 @@ void App::update()
|
|||||||
}
|
}
|
||||||
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
|
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
|
||||||
state_.pool_mining.xmrig_running = false;
|
state_.pool_mining.xmrig_running = false;
|
||||||
|
pool_starting_.store(false, std::memory_order_relaxed); // stopped / never came up
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-balance: periodically re-pick the pool by hashrate (self-throttled).
|
// Auto-balance: periodically re-pick the pool by hashrate (self-throttled).
|
||||||
@@ -1243,6 +1249,15 @@ void App::render()
|
|||||||
// Process deferred encryption from wizard (runs in background)
|
// Process deferred encryption from wizard (runs in background)
|
||||||
processDeferredEncryption();
|
processDeferredEncryption();
|
||||||
|
|
||||||
|
// Surface the blockchain-delete completion on the main thread (the worker can't touch Notifications).
|
||||||
|
{
|
||||||
|
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
|
||||||
|
if (deletedN >= 0) {
|
||||||
|
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) +
|
||||||
|
" items). The daemon is restarting to re-sync from the network.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run
|
// Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run
|
||||||
// before the sidebar reads current_page_ (below) so the forced page is reflected.
|
// before the sidebar reads current_page_ (below) so the forced page is reflected.
|
||||||
updateScreenshotSweep();
|
updateScreenshotSweep();
|
||||||
@@ -2263,6 +2278,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
static std::vector<std::pair<std::string, bool>> chips; // shuffled (word, consumed)
|
static std::vector<std::pair<std::string, bool>> chips; // shuffled (word, consumed)
|
||||||
static int progress = 0; // # words confirmed in order
|
static int progress = 0; // # words confirmed in order
|
||||||
static double wrongFlashUntil = 0.0; // brief "not the next word" hint
|
static double wrongFlashUntil = 0.0; // brief "not the next word" hint
|
||||||
|
static bool skipConfirm = false; // "Skip" backup requires a second confirming click
|
||||||
static bool creating = false; // async create (with failover) in flight
|
static bool creating = false; // async create (with failover) in flight
|
||||||
// Restore-from-seed step (step 3). The seed buffer is SECRET — wiped in finish() and on Back.
|
// Restore-from-seed step (step 3). The seed buffer is SECRET — wiped in finish() and on Back.
|
||||||
static char restoreSeed[512] = {};
|
static char restoreSeed[512] = {};
|
||||||
@@ -2290,6 +2306,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
chips.clear();
|
chips.clear();
|
||||||
progress = 0;
|
progress = 0;
|
||||||
step = 0;
|
step = 0;
|
||||||
|
skipConfirm = false;
|
||||||
creating = false;
|
creating = false;
|
||||||
lite_firstrun_dismissed_ = true;
|
lite_firstrun_dismissed_ = true;
|
||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
@@ -2400,14 +2417,28 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
std::mt19937 rng{std::random_device{}()};
|
std::mt19937 rng{std::random_device{}()};
|
||||||
std::shuffle(chips.begin(), chips.end(), rng);
|
std::shuffle(chips.begin(), chips.end(), rng);
|
||||||
progress = 0;
|
progress = 0;
|
||||||
|
skipConfirm = false;
|
||||||
step = 2;
|
step = 2;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
|
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton("Skip", ImVec2(80, 0))) {
|
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
|
||||||
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
|
if (!skipConfirm) {
|
||||||
finish();
|
skipConfirm = true; // require a second, deliberate click
|
||||||
|
} else {
|
||||||
|
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipConfirm) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
|
||||||
|
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
|
||||||
|
"Skip anyway?");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
}
|
}
|
||||||
} else if (step == 2) {
|
} else if (step == 2) {
|
||||||
// ── Verify: tap the words in order ──────────────────────────────────────
|
// ── Verify: tap the words in order ──────────────────────────────────────
|
||||||
@@ -2456,11 +2487,24 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
}
|
}
|
||||||
if (!verified) ImGui::EndDisabled();
|
if (!verified) ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton("Back", ImVec2(80, 0))) step = 1;
|
if (ui::material::TactileButton("Back", ImVec2(80, 0))) { skipConfirm = false; step = 1; }
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton("Skip", ImVec2(80, 0))) {
|
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
|
||||||
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
|
if (!skipConfirm) {
|
||||||
finish();
|
skipConfirm = true; // require a second, deliberate click
|
||||||
|
} else {
|
||||||
|
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipConfirm) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
|
||||||
|
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
|
||||||
|
"Skip anyway?");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
}
|
}
|
||||||
} else if (step == 3) {
|
} else if (step == 3) {
|
||||||
// ── Restore from an existing seed phrase ────────────────────────────────
|
// ── Restore from an existing seed phrase ────────────────────────────────
|
||||||
@@ -3344,6 +3388,8 @@ void App::deleteBlockchainData()
|
|||||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||||
auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||||
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems);
|
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems);
|
||||||
|
// Hand the count to the main thread for a completion toast (Notifications isn't thread-safe).
|
||||||
|
pending_delete_result_.store(result.deletedItems, std::memory_order_relaxed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3480,6 +3526,17 @@ void App::renderShutdownScreen()
|
|||||||
};
|
};
|
||||||
shutdown_timer_ += ImGui::GetIO().DeltaTime;
|
shutdown_timer_ += ImGui::GetIO().DeltaTime;
|
||||||
|
|
||||||
|
// Track how long the status text has been unchanged. A normal shutdown keeps updating the status
|
||||||
|
// ("Stopping pool miner…" -> "Flushing…" -> "Cleaning up…"); a genuine hang stalls it. Offer Force
|
||||||
|
// Quit only once the status has STALLED (likely hung), not on a bare timer, with a hard ceiling so
|
||||||
|
// it's never impossible to escape.
|
||||||
|
static std::string s_lastShutStatus;
|
||||||
|
static float s_shutStallTimer = 0.0f;
|
||||||
|
if (shutdown_status_ != s_lastShutStatus) { s_lastShutStatus = shutdown_status_; s_shutStallTimer = 0.0f; }
|
||||||
|
else s_shutStallTimer += ImGui::GetIO().DeltaTime;
|
||||||
|
const bool shutdownStalled = s_shutStallTimer >= 8.0f;
|
||||||
|
const bool allowForceQuit = shutdownStalled || shutdown_timer_ >= 20.0f;
|
||||||
|
|
||||||
// Use the main viewport so the overlay covers the primary window
|
// Use the main viewport so the overlay covers the primary window
|
||||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
ImVec2 vp_pos = vp->Pos;
|
ImVec2 vp_pos = vp->Pos;
|
||||||
@@ -3499,8 +3556,9 @@ void App::renderShutdownScreen()
|
|||||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||||
ImGuiWindowFlags_NoSavedSettings;
|
ImGuiWindowFlags_NoSavedSettings;
|
||||||
// Allow input after 10s so Force Quit button is clickable
|
// Allow input only once Force Quit is offered (status stalled / hard ceiling), so the screen stays
|
||||||
if (shutdown_timer_ < 10.0f)
|
// click-through while a normal shutdown is progressing.
|
||||||
|
if (!allowForceQuit)
|
||||||
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
||||||
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
||||||
|
|
||||||
@@ -3601,11 +3659,22 @@ void App::renderShutdownScreen()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// 4b. Force Quit button — appears after 10 seconds
|
// 4b. Force Quit button — appears once the shutdown appears stalled (or a hard-ceiling backstop)
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
if (shutdown_timer_ >= 10.0f) {
|
if (allowForceQuit) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
|
||||||
|
// chainstate; say so instead of a bare button.
|
||||||
|
if (shutdownStalled && !shutdown_status_.empty()) {
|
||||||
|
std::string stalledMsg = "Still \"" + shutdown_status_ + "\" — force quitting now may corrupt chain data.";
|
||||||
|
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
|
||||||
|
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||||
|
ImGui::TextUnformatted(stalledMsg.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
const char* forceLabel = TR("force_quit");
|
const char* forceLabel = TR("force_quit");
|
||||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
||||||
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||||
|
|||||||
@@ -601,6 +601,10 @@ private:
|
|||||||
// Daemon restart (e.g. after changing debug log categories)
|
// Daemon restart (e.g. after changing debug log categories)
|
||||||
std::atomic<bool> daemon_restarting_{false};
|
std::atomic<bool> daemon_restarting_{false};
|
||||||
|
|
||||||
|
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||||
|
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||||
|
std::atomic<int> pending_delete_result_{-1};
|
||||||
|
|
||||||
// Encryption state check timeout
|
// Encryption state check timeout
|
||||||
float encryption_check_timer_ = 0.0f;
|
float encryption_check_timer_ = 0.0f;
|
||||||
|
|
||||||
@@ -702,6 +706,10 @@ private:
|
|||||||
// Mining toggle guard (prevents concurrent setgenerate calls)
|
// Mining toggle guard (prevents concurrent setgenerate calls)
|
||||||
std::atomic<bool> mining_toggle_in_progress_{false};
|
std::atomic<bool> mining_toggle_in_progress_{false};
|
||||||
|
|
||||||
|
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
|
||||||
|
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
|
||||||
|
std::atomic<bool> pool_starting_{false};
|
||||||
|
|
||||||
// Auto-shield guard (prevents concurrent auto-shield operations)
|
// Auto-shield guard (prevents concurrent auto-shield operations)
|
||||||
std::atomic<bool> auto_shield_pending_{false};
|
std::atomic<bool> auto_shield_pending_{false};
|
||||||
|
|
||||||
|
|||||||
@@ -1949,6 +1949,10 @@ void App::startPoolMining(int threads)
|
|||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().error("Failed to start pool miner: " + err);
|
ui::Notifications::instance().error("Failed to start pool miner: " + err);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Miner spawned — it still needs a few seconds to connect to the pool and start hashing.
|
||||||
|
pool_starting_.store(true, std::memory_order_relaxed);
|
||||||
|
ui::Notifications::instance().info("Starting pool miner — connecting to the pool…");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2406,6 +2410,10 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
|
|||||||
else rpc_->call("importprivkey", {key, "", true}); // label "", rescan
|
else rpc_->call("importprivkey", {key, "", true}); // label "", rescan
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
err = e.what();
|
err = e.what();
|
||||||
|
} catch (...) {
|
||||||
|
// Non-std throw must not escape into the worker — the main-thread callback
|
||||||
|
// below would never run, leaving a stuck "Importing…" spinner.
|
||||||
|
err = "Import failed (unknown error)";
|
||||||
}
|
}
|
||||||
return [this, shielded, err, callback]() {
|
return [this, shielded, err, callback]() {
|
||||||
if (!err.empty()) {
|
if (!err.empty()) {
|
||||||
@@ -2558,6 +2566,10 @@ void App::submitZSendMany(const std::string& from, const std::string& to, double
|
|||||||
ok = true;
|
ok = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
result_str = e.what();
|
result_str = e.what();
|
||||||
|
} catch (...) {
|
||||||
|
// Non-std throw must not escape into the worker — the main-thread callback
|
||||||
|
// below would never run, leaving a stuck "Sending…" spinner.
|
||||||
|
result_str = "Send failed (unknown error)";
|
||||||
}
|
}
|
||||||
return [this, callback, ok, result_str, from, to, amount, fee, memo, markFeeGapRetry]() {
|
return [this, callback, ok, result_str, from, to, amount, fee, memo, markFeeGapRetry]() {
|
||||||
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
|
||||||
|
|||||||
@@ -1146,12 +1146,30 @@ void App::renderEncryptWalletDialog() {
|
|||||||
// Strength meter bar
|
// Strength meter bar
|
||||||
{
|
{
|
||||||
size_t len = strlen(encrypt_pass_buf_);
|
size_t len = strlen(encrypt_pass_buf_);
|
||||||
|
// Character-class diversity: an all-digit or single-class
|
||||||
|
// string shouldn't score as high as a mixed one.
|
||||||
|
bool hasDigit = false, hasLower = false, hasUpper = false, hasSymbol = false;
|
||||||
|
for (const char* c = encrypt_pass_buf_; *c; ++c) {
|
||||||
|
unsigned char uc = static_cast<unsigned char>(*c);
|
||||||
|
if (uc >= '0' && uc <= '9') hasDigit = true;
|
||||||
|
else if (uc >= 'a' && uc <= 'z') hasLower = true;
|
||||||
|
else if (uc >= 'A' && uc <= 'Z') hasUpper = true;
|
||||||
|
else hasSymbol = true;
|
||||||
|
}
|
||||||
|
int classes = (int)hasDigit + (int)hasLower + (int)hasUpper + (int)hasSymbol;
|
||||||
|
|
||||||
const char* strengthLabel = "Weak";
|
const char* strengthLabel = "Weak";
|
||||||
ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f);
|
ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f);
|
||||||
float strengthPct = 0.25f;
|
float strengthPct = 0.25f;
|
||||||
if (len >= 16) { strengthLabel = "Strong"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
|
int tier = 0; // 0=Weak, 1=Fair, 2=Good, 3=Strong
|
||||||
else if (len >= 12) { strengthLabel = "Good"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
|
if (len >= 16) tier = 3;
|
||||||
else if (len >= 8) { strengthLabel = "Fair"; strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
else if (len >= 12) tier = 2;
|
||||||
|
else if (len >= 8) tier = 1;
|
||||||
|
// Downgrade one tier when only a single character class is used.
|
||||||
|
if (classes <= 1 && tier > 0) tier -= 1;
|
||||||
|
if (tier == 3) { strengthLabel = "Strong"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
|
||||||
|
else if (tier == 2) { strengthLabel = "Good"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
|
||||||
|
else if (tier == 1) { strengthLabel = "Fair"; strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
||||||
|
|
||||||
float barW = ImGui::GetContentRegionAvail().x;
|
float barW = ImGui::GetContentRegionAvail().x;
|
||||||
float barH = 4.0f;
|
float barH = 4.0f;
|
||||||
|
|||||||
@@ -287,6 +287,19 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
|||||||
}
|
}
|
||||||
} liteSecretScrubber{input};
|
} liteSecretScrubber{input};
|
||||||
|
|
||||||
|
// Open/Restore both target a wallet path; reject an empty/whitespace-only one early so we
|
||||||
|
// never dispatch an unusable request (the scrubber above still wipes secrets on this return).
|
||||||
|
if (input.request.operation == wallet::LiteWalletLifecycleOperation::OpenExisting ||
|
||||||
|
input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) {
|
||||||
|
std::string trimmedPath(s_settingsState.lite_wallet_path);
|
||||||
|
const auto first = trimmedPath.find_first_not_of(" \t\r\n");
|
||||||
|
if (first == std::string::npos) {
|
||||||
|
s_settingsState.lite_lifecycle_status = "Enter a wallet path";
|
||||||
|
s_settingsState.lite_lifecycle_summary.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restore needs a complete 24-word seed; reject early (the scrubber above still wipes the
|
// Restore needs a complete 24-word seed; reject early (the scrubber above still wipes the
|
||||||
// entered secret on this return path).
|
// entered secret on this return path).
|
||||||
if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) {
|
if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) {
|
||||||
@@ -398,7 +411,15 @@ static void loadSettingsPageState(config::Settings* settings) {
|
|||||||
s_settingsState.verbose_logging = settings->getVerboseLogging();
|
s_settingsState.verbose_logging = settings->getVerboseLogging();
|
||||||
s_settingsState.debug_categories = settings->getDebugCategories();
|
s_settingsState.debug_categories = settings->getDebugCategories();
|
||||||
s_settingsState.debug_cats_dirty = false;
|
s_settingsState.debug_cats_dirty = false;
|
||||||
s_settingsState.rpc_plaintext_remote = rpc::Connection::usesPlaintextRemote(rpc::Connection::autoDetectConfig());
|
// Populate the RPC fields from the auto-detected daemon config so they show the REAL connection
|
||||||
|
// (host/port/user/pass) instead of compile-time defaults. These are read-only in the UI: the RPC
|
||||||
|
// credentials come from the daemon's DRAGONX.conf, so this is an accurate display, not an editor.
|
||||||
|
const auto rpcCfg = rpc::Connection::autoDetectConfig();
|
||||||
|
snprintf(s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), "%s", rpcCfg.host.c_str());
|
||||||
|
snprintf(s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), "%s", rpcCfg.port.c_str());
|
||||||
|
snprintf(s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), "%s", rpcCfg.rpcuser.c_str());
|
||||||
|
snprintf(s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), "%s", rpcCfg.rpcpassword.c_str());
|
||||||
|
s_settingsState.rpc_plaintext_remote = rpc::Connection::usesPlaintextRemote(rpcCfg);
|
||||||
|
|
||||||
// Apply loaded visual effects settings
|
// Apply loaded visual effects settings
|
||||||
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
|
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
|
||||||
@@ -2011,8 +2032,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::TextUnformatted(label);
|
ImGui::TextUnformatted(label);
|
||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
ImGui::SetNextItemWidth(inputW);
|
ImGui::SetNextItemWidth(inputW);
|
||||||
|
// Read-only: the RPC credentials are auto-detected from the daemon's DRAGONX.conf,
|
||||||
|
// so these fields DISPLAY the live connection (editing them here did nothing).
|
||||||
ImGui::InputText(id, buf, bufSz,
|
ImGui::InputText(id, buf, bufSz,
|
||||||
password ? ImGuiInputTextFlags_Password : 0);
|
ImGuiInputTextFlags_ReadOnly |
|
||||||
|
(password ? ImGuiInputTextFlags_Password : 0));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fourAcross) {
|
if (fourAcross) {
|
||||||
|
|||||||
@@ -221,7 +221,19 @@ public:
|
|||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
|
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
|
||||||
// Apply changes
|
// Apply changes
|
||||||
s_app->setAddressLabel(s_address, s_label);
|
// Trim surrounding whitespace; a whitespace-only label clears it
|
||||||
|
// (mirroring how an empty icon selection clears the icon).
|
||||||
|
std::string trimmedLabel(s_label);
|
||||||
|
{
|
||||||
|
std::size_t b = trimmedLabel.find_first_not_of(" \t\r\n");
|
||||||
|
if (b == std::string::npos) {
|
||||||
|
trimmedLabel.clear();
|
||||||
|
} else {
|
||||||
|
std::size_t e = trimmedLabel.find_last_not_of(" \t\r\n");
|
||||||
|
trimmedLabel = trimmedLabel.substr(b, e - b + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s_app->setAddressLabel(s_address, trimmedLabel);
|
||||||
if (s_selectedIcon >= 0)
|
if (s_selectedIcon >= 0)
|
||||||
s_app->setAddressIcon(s_address, material::project_icons::walletIconName(s_selectedIcon));
|
s_app->setAddressIcon(s_address, material::project_icons::walletIconName(s_selectedIcon));
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -226,7 +226,9 @@ public:
|
|||||||
Notifications::instance().error(result.empty() ? TR("transfer_failed") : result);
|
Notifications::instance().error(result.empty() ? TR("transfer_failed") : result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
s_open = false;
|
// Keep the dialog OPEN on submit: s_sending drives the button to a disabled "Sending…"
|
||||||
|
// state, and when the async callback sets s_resultMsg the in-dialog result screen shows
|
||||||
|
// (with its own Close button). Previously closing here made that result screen dead code.
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ void BlockInfoDialog::render(App* app)
|
|||||||
ImGui::SetNextItemWidth(heightInput.width);
|
ImGui::SetNextItemWidth(heightInput.width);
|
||||||
ImGui::InputInt("##Height", &s_height);
|
ImGui::InputInt("##Height", &s_height);
|
||||||
if (s_height < 1) s_height = 1;
|
if (s_height < 1) s_height = 1;
|
||||||
|
// Clamp to the chain tip so navigation/typing can't request a height
|
||||||
|
// above the tip (which would only yield a raw RPC error).
|
||||||
|
if (state.sync.blocks > 0 && s_height > state.sync.blocks) {
|
||||||
|
s_height = state.sync.blocks;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
@@ -287,7 +292,8 @@ void BlockInfoDialog::render(App* app)
|
|||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
material::Tooltip("%s", TR("block_click_next"));
|
material::Tooltip("%s", TR("block_click_next"));
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked()) {
|
if (ImGui::IsItemClicked() &&
|
||||||
|
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) {
|
||||||
s_height++;
|
s_height++;
|
||||||
s_has_data = false;
|
s_has_data = false;
|
||||||
}
|
}
|
||||||
@@ -308,7 +314,10 @@ void BlockInfoDialog::render(App* app)
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!s_next_hash.empty()) {
|
// Only offer "Next" when below the chain tip (the tip block has no
|
||||||
|
// nextblockhash, so this stays hidden there).
|
||||||
|
if (!s_next_hash.empty() &&
|
||||||
|
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) {
|
||||||
if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||||
s_height++;
|
s_height++;
|
||||||
s_has_data = false;
|
s_has_data = false;
|
||||||
|
|||||||
@@ -215,11 +215,15 @@ private:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Cancel button
|
// Cancel button — once clicked, disable it so the request is visibly
|
||||||
|
// acknowledged (and can't be re-fired) until the worker actually stops.
|
||||||
float btnW = 100.0f * dp;
|
float btnW = 100.0f * dp;
|
||||||
|
ImGui::BeginDisabled(s_cancelling);
|
||||||
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
|
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
|
||||||
|
s_cancelling = true;
|
||||||
s_bootstrap->cancel();
|
s_bootstrap->cancel();
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
// Check completion
|
// Check completion
|
||||||
if (s_bootstrap->isDone()) {
|
if (s_bootstrap->isDone()) {
|
||||||
@@ -234,6 +238,7 @@ private:
|
|||||||
s_state = State::Failed;
|
s_state = State::Failed;
|
||||||
}
|
}
|
||||||
s_bootstrap.reset();
|
s_bootstrap.reset();
|
||||||
|
s_cancelling = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +303,7 @@ private:
|
|||||||
s_bootstrap->start(dataDir, url);
|
s_bootstrap->start(dataDir, url);
|
||||||
s_state = State::Downloading;
|
s_state = State::Downloading;
|
||||||
s_errorMsg.clear();
|
s_errorMsg.clear();
|
||||||
|
s_cancelling = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline bool s_open = false;
|
static inline bool s_open = false;
|
||||||
@@ -305,6 +311,7 @@ private:
|
|||||||
static inline State s_state = State::Confirm;
|
static inline State s_state = State::Confirm;
|
||||||
static inline std::unique_ptr<util::Bootstrap> s_bootstrap;
|
static inline std::unique_ptr<util::Bootstrap> s_bootstrap;
|
||||||
static inline bool s_wasDaemonRunning = false;
|
static inline bool s_wasDaemonRunning = false;
|
||||||
|
static inline bool s_cancelling = false; // cancel requested; disables the Cancel button until the worker stops
|
||||||
static inline std::string s_errorMsg;
|
static inline std::string s_errorMsg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ static void applyBlockDetailResult(const json& result, const std::string& error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.is_null()) {
|
if (result.is_null()) {
|
||||||
s_search_error = "Invalid response from daemon";
|
s_search_error = TR("explorer_invalid_response");
|
||||||
s_show_detail_modal = false;
|
s_show_detail_modal = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -388,7 +388,7 @@ static void navigateToHash(App* app, const std::string& hash) {
|
|||||||
s_expanded_tx_idx = 0;
|
s_expanded_tx_idx = 0;
|
||||||
s_show_detail_modal = false;
|
s_show_detail_modal = false;
|
||||||
} else {
|
} else {
|
||||||
s_search_error = "No block or transaction found for this hash";
|
s_search_error = TR("explorer_hash_not_found");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -417,6 +417,11 @@ static void performSearch(App* app, const std::string& query) {
|
|||||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||||
});
|
});
|
||||||
if (isHex64) {
|
if (isHex64) {
|
||||||
|
// A hash lookup needs the daemon; navigateToHash() would otherwise return silently.
|
||||||
|
if (!app->rpc() || !app->rpc()->isConnected()) {
|
||||||
|
s_search_error = TR("explorer_not_connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
navigateToHash(app, query);
|
navigateToHash(app, query);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1039,6 +1044,9 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
// Nav buttons on same line
|
// Nav buttons on same line
|
||||||
ImGui::SameLine(contentW - Layout::spacingXl() * 3);
|
ImGui::SameLine(contentW - Layout::spacingXl() * 3);
|
||||||
|
|
||||||
|
// Disable prev/next while a fetch is in flight so navigation can't stack fetches.
|
||||||
|
ImGui::BeginDisabled(s_detail_loading);
|
||||||
|
|
||||||
// Prev
|
// Prev
|
||||||
if (s_detail_height > 1) {
|
if (s_detail_height > 1) {
|
||||||
ImGui::PushFont(Type().iconMed());
|
ImGui::PushFont(Type().iconMed());
|
||||||
@@ -1065,6 +1073,8 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ namespace ui {
|
|||||||
static bool s_open = false;
|
static bool s_open = false;
|
||||||
static char s_filename[256] = "";
|
static char s_filename[256] = "";
|
||||||
static std::string s_status;
|
static std::string s_status;
|
||||||
|
// Re-entrancy guard: true while a CSV write is in flight (disables the
|
||||||
|
// Export button so a second synchronous write can't be kicked off).
|
||||||
|
static bool s_exporting = false;
|
||||||
|
|
||||||
// Helper to escape CSV field
|
// Helper to escape CSV field
|
||||||
static std::string escapeCSV(const std::string& field)
|
static std::string escapeCSV(const std::string& field)
|
||||||
@@ -91,11 +94,13 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Export button
|
// Export button (disabled while a write is already in flight)
|
||||||
|
ImGui::BeginDisabled(s_exporting);
|
||||||
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||||
if (state.transactions.empty()) {
|
if (state.transactions.empty()) {
|
||||||
Notifications::instance().warning(TR("export_tx_none"));
|
Notifications::instance().warning(TR("export_tx_none"));
|
||||||
} else {
|
} else {
|
||||||
|
s_exporting = true;
|
||||||
std::string configDir = util::Platform::getConfigDir();
|
std::string configDir = util::Platform::getConfigDir();
|
||||||
std::string filepath = configDir + "/" + s_filename;
|
std::string filepath = configDir + "/" + s_filename;
|
||||||
|
|
||||||
@@ -142,8 +147,10 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
" transactions to: " + filepath;
|
" transactions to: " + filepath;
|
||||||
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
||||||
}
|
}
|
||||||
|
s_exporting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||||
|
|||||||
@@ -143,6 +143,15 @@ void KeyExportDialog::render(App* app)
|
|||||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching"));
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching"));
|
||||||
} else if (!s_error.empty()) {
|
} else if (!s_error.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
|
||||||
|
// Not a dead-end: clearing the error falls back through to the Reveal button next frame.
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (material::StyledButton(TR("retry"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||||
|
s_error.clear();
|
||||||
|
}
|
||||||
|
} else if (s_key.empty() && s_address.length() < 26) {
|
||||||
|
// Guard against an empty/blatantly-malformed address before dispatching a doomed reveal.
|
||||||
|
// (Also avoids reading s_address[0] on an empty string in the Reveal branch below.)
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("invalid_address"));
|
||||||
} else if (s_key.empty()) {
|
} else if (s_key.empty()) {
|
||||||
// Show button to fetch key
|
// Show button to fetch key
|
||||||
if (material::StyledButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
if (material::StyledButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ ThreadBenchmarkUpdate AdvanceThreadBenchmark(ThreadBenchmark& benchmark,
|
|||||||
update.startPoolMining = true;
|
update.startPoolMining = true;
|
||||||
update.startThreads = benchmark.optimal_threads;
|
update.startThreads = benchmark.optimal_threads;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No candidate produced a nonzero hashrate (e.g. a pool that never
|
||||||
|
// reported a rate). Don't finish silently — flag the run as
|
||||||
|
// inconclusive so the caller can surface it (keeps this core pure).
|
||||||
|
update.inconclusive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ struct ThreadBenchmarkUpdate {
|
|||||||
int startThreads = 0;
|
int startThreads = 0;
|
||||||
bool saveOptimalThreads = false;
|
bool saveOptimalThreads = false;
|
||||||
int optimalThreads = 0;
|
int optimalThreads = 0;
|
||||||
|
bool inconclusive = false; // finished with no nonzero hashrate sample — caller should warn
|
||||||
};
|
};
|
||||||
|
|
||||||
ThreadBenchmarkUpdate AdvanceThreadBenchmark(ThreadBenchmark& benchmark,
|
ThreadBenchmarkUpdate AdvanceThreadBenchmark(ThreadBenchmark& benchmark,
|
||||||
|
|||||||
@@ -176,8 +176,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
idleRightEdge = gBtnX - 4.0f * dp;
|
idleRightEdge = gBtnX - 4.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
|
// Idle delay combo renderer (shared by the non-scaling and thread-scaling
|
||||||
if (idleOn && !threadScaling) {
|
// branches below). Draws right-aligned at idleRightEdge and advances it.
|
||||||
|
auto renderIdleDelayCombo = [&](const char* comboId) {
|
||||||
struct DelayOption { int seconds; const char* label; };
|
struct DelayOption { int seconds; const char* label; };
|
||||||
static const DelayOption delays[] = {
|
static const DelayOption delays[] = {
|
||||||
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
||||||
@@ -192,7 +193,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
ImGui::SetNextItemWidth(comboW);
|
ImGui::SetNextItemWidth(comboW);
|
||||||
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
if (ImGui::BeginCombo(comboId, previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
||||||
for (const auto& d : delays) {
|
for (const auto& d : delays) {
|
||||||
bool selected = (d.seconds == curDelay);
|
bool selected = (d.seconds == curDelay);
|
||||||
if (ImGui::Selectable(d.label, selected)) {
|
if (ImGui::Selectable(d.label, selected)) {
|
||||||
@@ -206,6 +207,11 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
material::Tooltip("%s", TR("tt_idle_delay"));
|
material::Tooltip("%s", TR("tt_idle_delay"));
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
|
||||||
|
if (idleOn && !threadScaling) {
|
||||||
|
renderIdleDelayCombo("##IdleDelay");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread scaling controls: idle delay + active threads / idle threads combos
|
// Thread scaling controls: idle delay + active threads / idle threads combos
|
||||||
@@ -213,36 +219,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
||||||
|
|
||||||
// Idle delay combo
|
// Idle delay combo
|
||||||
{
|
renderIdleDelayCombo("##IdleDelayScale");
|
||||||
struct DelayOption { int seconds; const char* label; };
|
|
||||||
static const DelayOption delays[] = {
|
|
||||||
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
|
||||||
};
|
|
||||||
int curDelay = app->settings()->getMineIdleDelay();
|
|
||||||
const char* previewLabel = "2m";
|
|
||||||
for (const auto& d : delays) {
|
|
||||||
if (d.seconds == curDelay) { previewLabel = d.label; break; }
|
|
||||||
}
|
|
||||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
|
||||||
float comboX = idleRightEdge - comboW;
|
|
||||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
|
||||||
ImGui::SetNextItemWidth(comboW);
|
|
||||||
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
|
||||||
for (const auto& d : delays) {
|
|
||||||
bool selected = (d.seconds == curDelay);
|
|
||||||
if (ImGui::Selectable(d.label, selected)) {
|
|
||||||
app->settings()->setMineIdleDelay(d.seconds);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (selected) ImGui::SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
material::Tooltip("%s", TR("tt_idle_delay"));
|
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idle threads combo (threads when system is idle)
|
// Idle threads combo (threads when system is idle)
|
||||||
{
|
{
|
||||||
@@ -453,14 +430,27 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
// Require a wallet address for pool mining
|
// Require a wallet address for pool mining
|
||||||
std::string worker(s_pool_worker);
|
std::string worker(s_pool_worker);
|
||||||
if (!worker.empty()) {
|
if (!worker.empty()) {
|
||||||
s_benchmark.reset();
|
static bool s_benchConfirm = false;
|
||||||
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
|
if (!s_benchConfirm) {
|
||||||
s_benchmark.prev_threads = s_selected_threads;
|
// First click: build candidates so we can estimate the total duration, then
|
||||||
s_benchmark.buildCandidates(max_threads);
|
// require a confirming click (the benchmark interrupts mining and runs a while).
|
||||||
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
s_benchmark.reset();
|
||||||
// Stop any active solo mining first
|
s_benchmark.buildCandidates(max_threads);
|
||||||
if (mining.generate)
|
s_benchConfirm = true;
|
||||||
app->stopMining();
|
char msg[128];
|
||||||
|
snprintf(msg, sizeof(msg),
|
||||||
|
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
|
||||||
|
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
|
||||||
|
Notifications::instance().warning(msg);
|
||||||
|
} else {
|
||||||
|
// Second click: start (candidates already built above).
|
||||||
|
s_benchConfirm = false;
|
||||||
|
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
|
||||||
|
s_benchmark.prev_threads = s_selected_threads;
|
||||||
|
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||||
|
if (mining.generate)
|
||||||
|
app->stopMining();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,11 @@ static void RenderMiningTabContent(App* app)
|
|||||||
if (benchmarkUpdate.startPoolMining) {
|
if (benchmarkUpdate.startPoolMining) {
|
||||||
app->startPoolMining(benchmarkUpdate.startThreads);
|
app->startPoolMining(benchmarkUpdate.startThreads);
|
||||||
}
|
}
|
||||||
|
if (benchmarkUpdate.inconclusive) {
|
||||||
|
Notifications::instance().warning(
|
||||||
|
"Benchmark inconclusive: no hashrate samples were recorded. "
|
||||||
|
"Check the pool connection and try again.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|||||||
@@ -160,7 +160,12 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
applyAndRebuild();
|
applyAndRebuild();
|
||||||
}
|
}
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 110.0f * dp);
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 110.0f * dp);
|
||||||
if (TactileButton(TR("lite_net_refresh"), ImVec2(100.0f * dp, 0))) startProbe(st);
|
// While a probe is in flight, disable + relabel Refresh so there's clear in-progress feedback.
|
||||||
|
const bool probing = s_probe.busy();
|
||||||
|
ImGui::BeginDisabled(probing);
|
||||||
|
if (TactileButton(probing ? TR("lite_net_checking") : TR("lite_net_refresh"), ImVec2(100.0f * dp, 0)))
|
||||||
|
startProbe(st);
|
||||||
|
ImGui::EndDisabled();
|
||||||
if (randomMode)
|
if (randomMode)
|
||||||
Type().textColored(TypeStyle::Caption, Primary(), TR("lite_net_random_active"));
|
Type().textColored(TypeStyle::Caption, Primary(), TR("lite_net_random_active"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -191,7 +196,13 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
auto servers = st->getLiteServers();
|
auto servers = st->getLiteServers();
|
||||||
bool exists = false;
|
bool exists = false;
|
||||||
for (const auto& s : servers) if (s.url == url) { exists = true; break; }
|
for (const auto& s : servers) if (s.url == url) { exists = true; break; }
|
||||||
if (!exists) {
|
if (exists) {
|
||||||
|
// Duplicate: keep the inputs (so it doesn't look like the add succeeded); only
|
||||||
|
// clear any stale invalid-URL error since this URL is valid. The user-facing
|
||||||
|
// "already in list" message needs a shared i18n key added centrally, so no new
|
||||||
|
// text is set here.
|
||||||
|
s_addError.clear();
|
||||||
|
} else {
|
||||||
config::Settings::LiteServerPreference p;
|
config::Settings::LiteServerPreference p;
|
||||||
p.url = url;
|
p.url = url;
|
||||||
p.label = !addLabel.empty() ? addLabel : url;
|
p.label = !addLabel.empty() ? addLabel : url;
|
||||||
@@ -200,8 +211,8 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
st->setLiteServers(servers);
|
st->setLiteServers(servers);
|
||||||
st->save();
|
st->save();
|
||||||
startProbe(st);
|
startProbe(st);
|
||||||
|
s_addUrl[0] = '\0'; s_addLabel[0] = '\0'; s_addError.clear();
|
||||||
}
|
}
|
||||||
s_addUrl[0] = '\0'; s_addLabel[0] = '\0'; s_addError.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!s_addError.empty())
|
if (!s_addError.empty())
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ static int s_selected_banned_idx = -1;
|
|||||||
// Helper: Extract IP without port
|
// Helper: Extract IP without port
|
||||||
static std::string ExtractIP(const std::string& addr)
|
static std::string ExtractIP(const std::string& addr)
|
||||||
{
|
{
|
||||||
|
if (addr.empty()) return addr;
|
||||||
std::string ip = addr;
|
std::string ip = addr;
|
||||||
if (ip[0] == '[') {
|
if (ip[0] == '[') {
|
||||||
auto pos = ip.rfind("]:");
|
auto pos = ip.rfind("]:");
|
||||||
|
|||||||
@@ -290,27 +290,34 @@ static void RenderAddressDropdown(App* app, float width) {
|
|||||||
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), material::LoadingDots());
|
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), material::LoadingDots());
|
||||||
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
|
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
|
||||||
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
|
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||||
s_generating_address = true;
|
// createNewAddress can early-return without ever invoking the callback (e.g. connection
|
||||||
if (s_addr_type_filter != 2) {
|
// dropped between this button being enabled and the click landing), which would leave
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
// s_generating_address stuck. Only enter the "generating…" state when a dispatch is possible.
|
||||||
s_generating_address = false;
|
if (!app->isConnected()) {
|
||||||
if (addr.empty())
|
s_generating_address = false;
|
||||||
Notifications::instance().error(TR("failed_create_shielded"));
|
|
||||||
else {
|
|
||||||
s_pending_select_address = addr;
|
|
||||||
Notifications::instance().success(TR("new_shielded_created"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
app->createNewTAddress([](const std::string& addr) {
|
s_generating_address = true;
|
||||||
s_generating_address = false;
|
if (s_addr_type_filter != 2) {
|
||||||
if (addr.empty())
|
app->createNewZAddress([](const std::string& addr) {
|
||||||
Notifications::instance().error(TR("failed_create_transparent"));
|
s_generating_address = false;
|
||||||
else {
|
if (addr.empty())
|
||||||
s_pending_select_address = addr;
|
Notifications::instance().error(TR("failed_create_shielded"));
|
||||||
Notifications::instance().success(TR("new_transparent_created"));
|
else {
|
||||||
}
|
s_pending_select_address = addr;
|
||||||
});
|
Notifications::instance().success(TR("new_shielded_created"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
app->createNewTAddress([](const std::string& addr) {
|
||||||
|
s_generating_address = false;
|
||||||
|
if (addr.empty())
|
||||||
|
Notifications::instance().error(TR("failed_create_transparent"));
|
||||||
|
else {
|
||||||
|
s_pending_select_address = addr;
|
||||||
|
Notifications::instance().success(TR("new_transparent_created"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
@@ -601,6 +608,7 @@ void RenderReceiveTab(App* app)
|
|||||||
if (s_request_usd_mode && usd_price > 0) {
|
if (s_request_usd_mode && usd_price > 0) {
|
||||||
ImGui::PushItemWidth(amtInputW);
|
ImGui::PushItemWidth(amtInputW);
|
||||||
if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
|
if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
|
||||||
|
if (s_request_usd_amount < 0) s_request_usd_amount = 0;
|
||||||
s_request_amount = s_request_usd_amount / usd_price;
|
s_request_amount = s_request_usd_amount / usd_price;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -617,6 +625,7 @@ void RenderReceiveTab(App* app)
|
|||||||
} else {
|
} else {
|
||||||
ImGui::PushItemWidth(amtInputW);
|
ImGui::PushItemWidth(amtInputW);
|
||||||
if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
|
if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
|
||||||
|
if (s_request_amount < 0) s_request_amount = 0;
|
||||||
if (usd_price > 0)
|
if (usd_price > 0)
|
||||||
s_request_usd_amount = s_request_amount * usd_price;
|
s_request_usd_amount = s_request_amount * usd_price;
|
||||||
}
|
}
|
||||||
@@ -763,9 +772,14 @@ void RenderReceiveTab(App* app)
|
|||||||
ImVec2(addrColW, memoInputH));
|
ImVec2(addrColW, memoInputH));
|
||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
|
// The Sapling memo limit is in BYTES and matches the enforced buffer size
|
||||||
|
// (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
|
||||||
|
// old display-only 256 value, and warn in colour once the field is at its cap.
|
||||||
size_t memo_len = strlen(s_request_memo);
|
size_t memo_len = strlen(s_request_memo);
|
||||||
snprintf(buf, sizeof(buf), "%zu / %d", memo_len, (int)S.drawElement("tabs.receive", "memo-max-display-chars").size);
|
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
bool memoAtCap = memo_len + 1 >= memoMax;
|
||||||
|
snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax);
|
||||||
|
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// URI preview
|
// URI preview
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ static char s_address[512] = "";
|
|||||||
static double s_amount = 0.0;
|
static double s_amount = 0.0;
|
||||||
static char s_memo[512] = "";
|
static char s_memo[512] = "";
|
||||||
static char s_label[128] = "";
|
static char s_label[128] = "";
|
||||||
static int s_selected_addr_idx = -1;
|
|
||||||
static std::string s_payment_uri;
|
static std::string s_payment_uri;
|
||||||
static uintptr_t s_qr_texture = 0;
|
static uintptr_t s_qr_texture = 0;
|
||||||
static int s_qr_width = 0;
|
static int s_qr_width = 0;
|
||||||
@@ -45,7 +44,6 @@ void RequestPaymentDialog::show(const std::string& address)
|
|||||||
s_amount = 0.0;
|
s_amount = 0.0;
|
||||||
s_memo[0] = '\0';
|
s_memo[0] = '\0';
|
||||||
s_label[0] = '\0';
|
s_label[0] = '\0';
|
||||||
s_selected_addr_idx = -1;
|
|
||||||
s_uri_dirty = true;
|
s_uri_dirty = true;
|
||||||
|
|
||||||
if (!address.empty()) {
|
if (!address.empty()) {
|
||||||
@@ -107,7 +105,7 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate);
|
label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::Selectable(label.c_str(), s_address == addr.address)) {
|
if (ImGui::Selectable(label.c_str(), addr.address == s_address)) {
|
||||||
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
|
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
|
||||||
s_uri_dirty = true;
|
s_uri_dirty = true;
|
||||||
}
|
}
|
||||||
@@ -124,7 +122,7 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
label = label.substr(0, tAddrFrontLbl.truncate) + "..." + label.substr(label.length() - tAddrBackLbl.truncate);
|
label = label.substr(0, tAddrFrontLbl.truncate) + "..." + label.substr(label.length() - tAddrBackLbl.truncate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::Selectable(label.c_str(), s_address == addr.address)) {
|
if (ImGui::Selectable(label.c_str(), addr.address == s_address)) {
|
||||||
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
|
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
|
||||||
s_uri_dirty = true;
|
s_uri_dirty = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -725,7 +725,17 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
{
|
{
|
||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_network_fee"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_network_fee"));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
double feeBefore = s_fee;
|
||||||
RenderFeeTierSelector("##confirm");
|
RenderFeeTierSelector("##confirm");
|
||||||
|
// If Max was selected, a fee bump here would push total over the available
|
||||||
|
// balance (s_amount was clamped to available - old fee). Re-clamp so the send
|
||||||
|
// stays within budget.
|
||||||
|
if (s_send_max && s_fee != feeBefore) {
|
||||||
|
double avail = GetAvailableBalance(app);
|
||||||
|
double maxAmount = avail - s_fee;
|
||||||
|
if (maxAmount < 0) maxAmount = 0;
|
||||||
|
s_amount = maxAmount;
|
||||||
|
}
|
||||||
// Recalculate total after potential fee change
|
// Recalculate total after potential fee change
|
||||||
total = s_amount + s_fee;
|
total = s_amount + s_fee;
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
@@ -1129,9 +1139,16 @@ void RenderSendTab(App* app)
|
|||||||
|
|
||||||
// Handle pending payment from URI
|
// Handle pending payment from URI
|
||||||
if (app->hasPendingPayment()) {
|
if (app->hasPendingPayment()) {
|
||||||
|
// A URI arriving mid-compose would silently overwrite the user's in-progress form.
|
||||||
|
// Capture an undo snapshot first so "Undo" can restore what they were typing.
|
||||||
|
if (FormHasData()) {
|
||||||
|
SaveFormSnapshot();
|
||||||
|
}
|
||||||
strncpy(s_to_address, app->getPendingToAddress().c_str(), sizeof(s_to_address) - 1);
|
strncpy(s_to_address, app->getPendingToAddress().c_str(), sizeof(s_to_address) - 1);
|
||||||
|
s_to_address[sizeof(s_to_address) - 1] = '\0';
|
||||||
s_amount = app->getPendingAmount();
|
s_amount = app->getPendingAmount();
|
||||||
strncpy(s_memo, app->getPendingMemo().c_str(), sizeof(s_memo) - 1);
|
strncpy(s_memo, app->getPendingMemo().c_str(), sizeof(s_memo) - 1);
|
||||||
|
s_memo[sizeof(s_memo) - 1] = '\0';
|
||||||
app->clearPendingPayment();
|
app->clearPendingPayment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ void ShieldDialog::render(App* app)
|
|||||||
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
|
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
|
||||||
if (material::StyledButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
|
if (material::StyledButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
|
||||||
s_operation_pending = true;
|
s_operation_pending = true;
|
||||||
s_status_message = "Submitting operation...";
|
s_status_message = TR("shield_submitting");
|
||||||
|
|
||||||
if (s_mode == Mode::ShieldCoinbase) {
|
if (s_mode == Mode::ShieldCoinbase) {
|
||||||
std::string from(s_from_address), to(s_to_address);
|
std::string from(s_from_address), to(s_to_address);
|
||||||
@@ -201,14 +201,14 @@ void ShieldDialog::render(App* app)
|
|||||||
s_operation_pending = false;
|
s_operation_pending = false;
|
||||||
if (error.empty()) {
|
if (error.empty()) {
|
||||||
s_operation_id = result.value("opid", "");
|
s_operation_id = result.value("opid", "");
|
||||||
s_status_message = "Operation submitted: " + s_operation_id;
|
s_status_message = std::string(TR("shield_op_submitted")) + s_operation_id;
|
||||||
Notifications::instance().success(TR("shield_started"));
|
Notifications::instance().success(TR("shield_started"));
|
||||||
// Register with the shared poller so an async failure is
|
// Register with the shared poller so an async failure is
|
||||||
// surfaced (and balances refresh) even after this dialog closes.
|
// surfaced (and balances refresh) even after this dialog closes.
|
||||||
app->trackOperation(s_operation_id);
|
app->trackOperation(s_operation_id);
|
||||||
} else {
|
} else {
|
||||||
s_status_message = "Error: " + error;
|
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
||||||
Notifications::instance().error("Shield failed: " + error);
|
Notifications::instance().error(std::string(TR("shield_send_failed")) + error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -235,14 +235,14 @@ void ShieldDialog::render(App* app)
|
|||||||
s_operation_pending = false;
|
s_operation_pending = false;
|
||||||
if (error.empty()) {
|
if (error.empty()) {
|
||||||
s_operation_id = result.value("opid", "");
|
s_operation_id = result.value("opid", "");
|
||||||
s_status_message = "Operation submitted: " + s_operation_id;
|
s_status_message = std::string(TR("shield_op_submitted")) + s_operation_id;
|
||||||
Notifications::instance().success(TR("merge_started"));
|
Notifications::instance().success(TR("merge_started"));
|
||||||
// Register with the shared poller so an async failure is
|
// Register with the shared poller so an async failure is
|
||||||
// surfaced (and balances refresh) even after this dialog closes.
|
// surfaced (and balances refresh) even after this dialog closes.
|
||||||
app->trackOperation(s_operation_id);
|
app->trackOperation(s_operation_id);
|
||||||
} else {
|
} else {
|
||||||
s_status_message = "Error: " + error;
|
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
||||||
Notifications::instance().error("Merge failed: " + error);
|
Notifications::instance().error(std::string(TR("merge_send_failed")) + error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -293,16 +293,16 @@ void ShieldDialog::render(App* app)
|
|||||||
s_status_message = TR("shield_completed");
|
s_status_message = TR("shield_completed");
|
||||||
Notifications::instance().success(TR("shield_merge_done"));
|
Notifications::instance().success(TR("shield_merge_done"));
|
||||||
} else if (status == "failed") {
|
} else if (status == "failed") {
|
||||||
std::string errMsg = op.value("error", nlohmann::json{}).value("message", "Unknown error");
|
std::string errMsg = op.value("error", nlohmann::json{}).value("message", TR("shield_unknown_error"));
|
||||||
s_status_message = "Operation failed: " + errMsg;
|
s_status_message = std::string(TR("shield_op_failed")) + errMsg;
|
||||||
Notifications::instance().error("Operation failed: " + errMsg);
|
Notifications::instance().error(std::string(TR("shield_op_failed")) + errMsg);
|
||||||
} else if (status == "executing") {
|
} else if (status == "executing") {
|
||||||
s_status_message = TR("shield_in_progress");
|
s_status_message = TR("shield_in_progress");
|
||||||
} else {
|
} else {
|
||||||
s_status_message = "Status: " + status;
|
s_status_message = std::string(TR("shield_status_label")) + status;
|
||||||
}
|
}
|
||||||
} else if (!error.empty()) {
|
} else if (!error.empty()) {
|
||||||
s_status_message = "Error checking status: " + error;
|
s_status_message = std::string(TR("shield_status_check_error")) + error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -179,10 +179,15 @@ void TransactionDetailsDialog::render(App* app)
|
|||||||
float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f;
|
float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f;
|
||||||
ImGui::SetCursorPosX(start_x);
|
ImGui::SetCursorPosX(start_x);
|
||||||
|
|
||||||
|
// Guard against an empty/whitespace explorer URL so we never open a garbage link.
|
||||||
|
std::string explorerBase = app->settings()->getTxExplorerUrl();
|
||||||
|
bool explorerValid = explorerBase.find_first_not_of(" \t\r\n") != std::string::npos;
|
||||||
|
if (!explorerValid) ImGui::BeginDisabled();
|
||||||
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||||
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
|
std::string url = explorerBase + tx.txid;
|
||||||
util::Platform::openUrl(url);
|
util::Platform::openUrl(url);
|
||||||
}
|
}
|
||||||
|
if (!explorerValid) ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
|||||||
@@ -1501,6 +1501,15 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["shield_to_address"] = "To Address (Shielded):";
|
strings_["shield_to_address"] = "To Address (Shielded):";
|
||||||
strings_["shield_utxo_limit"] = "UTXO Limit:";
|
strings_["shield_utxo_limit"] = "UTXO Limit:";
|
||||||
strings_["shield_wildcard_hint"] = "Use '*' to shield from all transparent addresses";
|
strings_["shield_wildcard_hint"] = "Use '*' to shield from all transparent addresses";
|
||||||
|
strings_["shield_submitting"] = "Submitting operation...";
|
||||||
|
strings_["shield_op_submitted"] = "Operation submitted: ";
|
||||||
|
strings_["shield_op_failed"] = "Operation failed: ";
|
||||||
|
strings_["shield_error_prefix"] = "Error: ";
|
||||||
|
strings_["shield_status_label"] = "Status: ";
|
||||||
|
strings_["shield_status_check_error"] = "Error checking status: ";
|
||||||
|
strings_["shield_unknown_error"] = "Unknown error";
|
||||||
|
strings_["shield_send_failed"] = "Shield failed: ";
|
||||||
|
strings_["merge_send_failed"] = "Merge failed: ";
|
||||||
strings_["merge_description"] = "Merge multiple UTXOs into a single shielded address. This can help reduce wallet size and improve privacy.";
|
strings_["merge_description"] = "Merge multiple UTXOs into a single shielded address. This can help reduce wallet size and improve privacy.";
|
||||||
strings_["merge_funds"] = "Merge Funds";
|
strings_["merge_funds"] = "Merge Funds";
|
||||||
strings_["merge_started"] = "Merge operation started";
|
strings_["merge_started"] = "Merge operation started";
|
||||||
@@ -1568,6 +1577,9 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["explorer_tx_outputs"] = "Outputs";
|
strings_["explorer_tx_outputs"] = "Outputs";
|
||||||
strings_["explorer_tx_size"] = "Size";
|
strings_["explorer_tx_size"] = "Size";
|
||||||
strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash";
|
strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash";
|
||||||
|
strings_["explorer_invalid_response"] = "Invalid response from daemon";
|
||||||
|
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
|
||||||
|
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
|
||||||
strings_["explorer_no_results"] = "No matching cached blocks";
|
strings_["explorer_no_results"] = "No matching cached blocks";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user