diff --git a/src/app.h b/src/app.h index 685b21b..ca942b8 100644 --- a/src/app.h +++ b/src/app.h @@ -1927,6 +1927,7 @@ private: // First-run wizard state WizardPhase wizard_phase_ = WizardPhase::None; std::unique_ptr bootstrap_; + std::string wizard_bootstrap_url_; // source the wizard last started ("" = default); Retry keeps it bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active std::string wizard_pending_passphrase_; // held until daemon connects std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped diff --git a/src/app_wizard.cpp b/src/app_wizard.cpp index 6ebd4cd..6135c91 100644 --- a/src/app_wizard.cpp +++ b/src/app_wizard.cpp @@ -800,21 +800,22 @@ void App::renderFirstRunWizard() { // Daemon status indicator { bool daemonUp = isEmbeddedDaemonRunning(); - const std::string& dStatus = getDaemonStatus(); - ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) // green - : IM_COL32(120, 120, 120, 160); // gray - if (dStatus.find("Stopping") != std::string::npos) - dotCol = IM_COL32(255, 167, 38, 200); // orange + // Key the dot + label off the daemon state ENUM, not an English substring of the status + // text — the substring match silently failed under every non-English language. + const bool daemonStopping = daemon_controller_ && + daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping; + ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230) // orange + : daemonUp ? IM_COL32(76, 175, 80, 230) // green + : IM_COL32(120, 120, 120, 180); // gray float dotR = 3.5f * dp; dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f), dotR, dotCol); - const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos - ? TR("bootstrap_daemon_stopping") - : TR("bootstrap_daemon_running")) - : TR("bootstrap_daemon_stopped"); + const char* label = daemonStopping ? TR("bootstrap_daemon_stopping") + : daemonUp ? TR("bootstrap_daemon_running") + : TR("bootstrap_daemon_stopped"); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy), - (dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label); + (dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label); cy += captionFont->LegacySize + 6.0f * dp; } @@ -862,14 +863,19 @@ void App::renderFirstRunWizard() { ui::material::Error(), TR("wiz_download_failed")); cy += bodyFont->LegacySize + 8.0f * dp; + // Wrap the (often long) error within the card instead of letting it clip off the edge. dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, - errMsg.c_str()); - cy += captionFont->LegacySize + 16.0f * dp; + errMsg.c_str(), nullptr, contentW); + ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, errMsg.c_str()); + cy += es.y + 16.0f * dp; - // Retry / Skip + // Retry / (Try mirror) / Skip. Offer the mirror unless the last attempt already used it. + const std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName; + const bool showMirror = (wizard_bootstrap_url_ != mirrorUrl); float btnW2 = 120.0f * dp; float btnH2 = 40.0f * dp; - float totalBW = btnW2 * 2 + 12.0f * dp; + const int nBtn = showMirror ? 3 : 2; + float totalBW = btnW2 * nBtn + 12.0f * dp * (nBtn - 1); float bx = rightX + (colW - totalBW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(bx, cy)); @@ -883,14 +889,33 @@ void App::renderFirstRunWizard() { stopDaemonForBootstrap(); bootstrap_ = std::make_unique(); std::string dataDir = util::Platform::getDragonXDataDir(); - bootstrap_->start(dataDir); + if (wizard_bootstrap_url_.empty()) bootstrap_->start(dataDir); + else bootstrap_->start(dataDir, wizard_bootstrap_url_); // keep the user's chosen source wizard_phase_ = WizardPhase::BootstrapInProgress; } ImGui::EndDisabled(); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); - ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy)); + float nextBx = bx + btnW2 + 12.0f * dp; + if (showMirror) { + ImGui::SetCursorScreenPos(ImVec2(nextBx, cy)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); + ImGui::BeginDisabled(!supportsFullNodeLifecycleActions()); + if (ui::material::TactileButton(TR("bootstrap_try_mirror"), ImVec2(btnW2, btnH2)) && !refuseIfNodeBusy()) { + stopDaemonForBootstrap(); + bootstrap_ = std::make_unique(); + bootstrap_->start(util::Platform::getDragonXDataDir(), mirrorUrl); + wizard_bootstrap_url_ = mirrorUrl; + wizard_phase_ = WizardPhase::BootstrapInProgress; + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered()) ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip")); + ImGui::PopStyleVar(); + nextBx += btnW2 + 12.0f * dp; + } + + ImGui::SetCursorScreenPos(ImVec2(nextBx, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW2, btnH2))) { wizard_phase_ = WizardPhase::EncryptOffer; @@ -1014,6 +1039,15 @@ void App::renderFirstRunWizard() { cy += bsSize.y + 8.0f * dp; } + // Honest size/time hint (no number is known until the download HEADs the remote file). + { + const char* hintText = TR("wiz_bootstrap_size_hint"); + ImVec2 hs = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, hintText); + dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), + (dimCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 200), hintText, nullptr, contentW); + cy += hs.y + 8.0f * dp; + } + // Trust warning { float warnOpacity = S.drawElement("screens.first-run", "trust-warning").opacity; @@ -1031,21 +1065,20 @@ void App::renderFirstRunWizard() { // Daemon status indicator (subtle, before buttons) { bool daemonUp = isEmbeddedDaemonRunning(); - const std::string& dStatus = getDaemonStatus(); - ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) - : IM_COL32(120, 120, 120, 160); - if (dStatus.find("Stopping") != std::string::npos) - dotCol = IM_COL32(255, 167, 38, 200); + const bool daemonStopping = daemon_controller_ && + daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping; + ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230) + : daemonUp ? IM_COL32(76, 175, 80, 230) + : IM_COL32(120, 120, 120, 180); float dotR = 3.5f * dp; dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f), dotR, dotCol); - const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos - ? TR("bootstrap_daemon_stopping") - : TR("bootstrap_daemon_running")) - : TR("bootstrap_daemon_stopped"); + const char* label = daemonStopping ? TR("bootstrap_daemon_stopping") + : daemonUp ? TR("bootstrap_daemon_running") + : TR("bootstrap_daemon_stopped"); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy), - (dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label); + (dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label); cy += captionFont->LegacySize + 10.0f * dp; } @@ -1071,6 +1104,7 @@ void App::renderFirstRunWizard() { bootstrap_ = std::make_unique(); std::string dataDir = util::Platform::getDragonXDataDir(); bootstrap_->start(dataDir); + wizard_bootstrap_url_.clear(); // default source; Retry won't force the mirror wizard_phase_ = WizardPhase::BootstrapInProgress; } ImGui::EndDisabled(); @@ -1084,17 +1118,18 @@ void App::renderFirstRunWizard() { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::BeginDisabled(!supportsFullNodeLifecycleActions()); - if (ui::material::TactileButton(TR("bootstrap_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) { + if (ui::material::TactileButton(TR("bootstrap_use_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) { stopDaemonForBootstrap(); bootstrap_ = std::make_unique(); std::string dataDir = util::Platform::getDragonXDataDir(); std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName; bootstrap_->start(dataDir, mirrorUrl); + wizard_bootstrap_url_ = mirrorUrl; // Retry on the failed card keeps this source wizard_phase_ = WizardPhase::BootstrapInProgress; } ImGui::EndDisabled(); if (ImGui::IsItemHovered()) { - ui::material::Tooltip(TR("bootstrap_mirror_tooltip")); + ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip")); } ImGui::PopStyleVar(); ImGui::PopStyleColor(3); diff --git a/src/ui/windows/bootstrap_download_dialog.h b/src/ui/windows/bootstrap_download_dialog.h index abccb96..9b81c28 100644 --- a/src/ui/windows/bootstrap_download_dialog.h +++ b/src/ui/windows/bootstrap_download_dialog.h @@ -283,13 +283,17 @@ private: Type().textColored(TypeStyle::H6, Success(), TR("bootstrap_success")); ImGui::Spacing(); - ImGui::TextWrapped("%s", TR("bootstrap_success_desc")); + // For the embedded daemon, tell the user the node will start (and offer the button) instead of + // instructing them to start it by hand. External-daemon users keep the manual-start wording. + const bool embedded = s_app->isUsingEmbeddedDaemon(); + ImGui::TextWrapped("%s", TR(embedded ? "bootstrap_success_desc_start" : "bootstrap_success_desc")); ImGui::Spacing(); ImGui::Spacing(); - float btnW = 140.0f * dp; - if (s_wasDaemonRunning) { - if (TactileButton(TR("bootstrap_restart_daemon"), ImVec2(btnW, 0))) { + float btnW = 150.0f * dp; + if (s_wasDaemonRunning || embedded) { + if (TactileButton(s_wasDaemonRunning ? TR("bootstrap_restart_daemon") : TR("bootstrap_start_daemon"), + ImVec2(btnW, 0))) { s_app->setBootstrapDownloading(false); s_app->startEmbeddedDaemon(); s_open = false; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index b56d310..2a41c18 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -899,6 +899,9 @@ void I18n::loadBuiltinEnglish() strings_["bootstrap_trust_warning"] = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node."; strings_["bootstrap_mirror"] = "Mirror"; strings_["bootstrap_mirror_tooltip"] = "Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing."; + strings_["bootstrap_use_mirror"] = "Use mirror"; + strings_["bootstrap_use_mirror_tooltip"] = "Download the same bootstrap from the backup server (bootstrap2.dragonx.is) instead of the main one (bootstrap.dragonx.is).\nChoose this if the main download is slow or failing."; + strings_["wiz_bootstrap_size_hint"] = "This is a large download. The exact size and a time estimate appear once it starts, and it can be resumed if interrupted."; strings_["bootstrap_downloading"] = "Downloading bootstrap..."; strings_["bootstrap_verifying"] = "Verifying checksums..."; strings_["bootstrap_extracting"] = "Extracting blockchain data..."; @@ -909,6 +912,8 @@ void I18n::loadBuiltinEnglish() strings_["bootstrap_success"] = "Bootstrap Complete"; strings_["bootstrap_success_desc"] = "Blockchain data has been extracted successfully. Start the daemon to begin syncing from the bootstrap point."; strings_["bootstrap_restart_daemon"] = "Restart Daemon"; + strings_["bootstrap_start_daemon"] = "Start Daemon"; + strings_["bootstrap_success_desc_start"] = "Blockchain data was extracted successfully. Click Start Daemon to begin syncing from the bootstrap point."; strings_["bootstrap_failed"] = "Bootstrap Failed"; strings_["tt_open_dir"] = "Click to open in file explorer"; strings_["settings_open_data_dir"] = "Open data folder"; @@ -2323,6 +2328,7 @@ void I18n::loadBuiltinEnglish() strings_["balance_mining_fmt"] = "Mining rewards: %.8f"; strings_["mining_card_tooltip"] = "Solo-mining rewards arrive at your mining (transparent, R…) addresses. Those addresses can't receive payments. Rewards are shielded automatically once they mature."; strings_["mining_card_click_to_shield"] = "Click to shield matured rewards now."; + strings_["shielded_card_tooltip"] = "Shielded (private) balance: funds held in z-addresses. Amounts, senders and recipients are hidden on the blockchain. Click to open Receive."; strings_["contact_chip_mining"] = "Mining only"; strings_["failed_create_mining_address"] = "Failed to create mining address"; strings_["new_mining_address_created"] = "New mining address created";