diff --git a/res/lang/de.json b/res/lang/de.json index 4bb9d79..9f3ad46 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -1294,6 +1294,13 @@ "sweep_to": "Gefegt an:", "sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)", "sweep_tx": "Transaktion:", + "switch_progress_background": "Im Hintergrund fortsetzen", + "switch_progress_failed_title": "Wallet-Wechsel fehlgeschlagen", + "switch_progress_hint": "Ein sauberes Herunterfahren kann bis zu einer Minute dauern.", + "switch_progress_reconnecting": "Neu verbinden", + "switch_progress_starting": "Knoten wird mit der neuen Wallet gestartet", + "switch_progress_stopping": "Aktueller Knoten wird gestoppt", + "switch_progress_title": "Wallet wird gewechselt", "switch_stopnode_body": "Beim Wallet-Wechsel wird der Knoten mit der ausgewählten Wallet neu gestartet. Der laufende Knoten wird gestoppt und mit der neuen Wallet neu gestartet – wenn Sie ihn absichtlich laufen ließen, startet er automatisch wieder.", "switch_stopnode_confirm": "Knoten stoppen & wechseln", "switch_stopnode_title": "Laufenden Knoten stoppen?", diff --git a/res/lang/es.json b/res/lang/es.json index 416d087..34fcaea 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -1294,6 +1294,13 @@ "sweep_to": "Barrido a:", "sweep_toggle": "Barrer a mi monedero (no conservar la clave)", "sweep_tx": "Transacción:", + "switch_progress_background": "Continuar en segundo plano", + "switch_progress_failed_title": "Error al cambiar de cartera", + "switch_progress_hint": "Un apagado ordenado puede tardar hasta un minuto.", + "switch_progress_reconnecting": "Reconectando", + "switch_progress_starting": "Iniciando el nodo con la nueva cartera", + "switch_progress_stopping": "Deteniendo el nodo actual", + "switch_progress_title": "Cambiando de cartera", "switch_stopnode_body": "Cambiar de cartera reinicia el nodo con la cartera seleccionada. El nodo en ejecución se detendrá y se reiniciará con la nueva cartera; si lo dejaste en marcha a propósito, volverá a iniciarse automáticamente.", "switch_stopnode_confirm": "Detener nodo y cambiar", "switch_stopnode_title": "¿Detener el nodo en ejecución?", diff --git a/res/lang/fr.json b/res/lang/fr.json index f9f6f91..006567f 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -1294,6 +1294,13 @@ "sweep_to": "Balayé vers :", "sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)", "sweep_tx": "Transaction :", + "switch_progress_background": "Continuer en arrière-plan", + "switch_progress_failed_title": "Échec du changement de portefeuille", + "switch_progress_hint": "Un arrêt propre peut prendre jusqu'à une minute.", + "switch_progress_reconnecting": "Reconnexion", + "switch_progress_starting": "Démarrage du nœud sur le nouveau portefeuille", + "switch_progress_stopping": "Arrêt du nœud actuel", + "switch_progress_title": "Changement de portefeuille", "switch_stopnode_body": "Changer de portefeuille redémarre le nœud sur le portefeuille sélectionné. Le nœud en cours sera arrêté puis relancé sur le nouveau portefeuille — si vous l'avez laissé tourner exprès, il redémarre automatiquement.", "switch_stopnode_confirm": "Arrêter le nœud et changer", "switch_stopnode_title": "Arrêter le nœud en cours d'exécution ?", diff --git a/res/lang/ja.json b/res/lang/ja.json index 88a83a3..2653376 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -1294,6 +1294,13 @@ "sweep_to": "集約先:", "sweep_toggle": "ウォレットに集約(鍵は保持しない)", "sweep_tx": "取引:", + "switch_progress_background": "バックグラウンドで続行", + "switch_progress_failed_title": "ウォレットの切り替えに失敗しました", + "switch_progress_hint": "正常なシャットダウンには最大1分かかることがあります。", + "switch_progress_reconnecting": "再接続しています", + "switch_progress_starting": "新しいウォレットでノードを起動しています", + "switch_progress_stopping": "現在のノードを停止しています", + "switch_progress_title": "ウォレットを切り替え中", "switch_stopnode_body": "ウォレットを切り替えると、選択したウォレットでノードが再起動します。実行中のノードは停止され、新しいウォレットで再起動されます。意図的に起動したままにしていた場合は、自動的に復帰します。", "switch_stopnode_confirm": "ノードを停止して切り替え", "switch_stopnode_title": "実行中のノードを停止しますか?", diff --git a/res/lang/ko.json b/res/lang/ko.json index bf08637..664e209 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -1294,6 +1294,13 @@ "sweep_to": "쓸어담은 주소:", "sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)", "sweep_tx": "거래:", + "switch_progress_background": "백그라운드에서 계속", + "switch_progress_failed_title": "지갑 전환 실패", + "switch_progress_hint": "정상 종료에는 최대 1분이 걸릴 수 있습니다.", + "switch_progress_reconnecting": "다시 연결하는 중", + "switch_progress_starting": "새 지갑으로 노드를 시작하는 중", + "switch_progress_stopping": "현재 노드를 중지하는 중", + "switch_progress_title": "지갑 전환 중", "switch_stopnode_body": "지갑을 전환하면 선택한 지갑으로 노드가 다시 시작됩니다. 실행 중인 노드가 중지되고 새 지갑으로 다시 시작됩니다. 일부러 계속 실행해 두었다면 자동으로 다시 켜집니다.", "switch_stopnode_confirm": "노드 중지 후 전환", "switch_stopnode_title": "실행 중인 노드를 중지할까요?", diff --git a/res/lang/pt.json b/res/lang/pt.json index e9810c0..f611394 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -1294,6 +1294,13 @@ "sweep_to": "Varrido para:", "sweep_toggle": "Varrer para minha carteira (não manter a chave)", "sweep_tx": "Transação:", + "switch_progress_background": "Continuar em segundo plano", + "switch_progress_failed_title": "Falha ao trocar de carteira", + "switch_progress_hint": "Um desligamento normal pode levar até um minuto.", + "switch_progress_reconnecting": "Reconectando", + "switch_progress_starting": "Iniciando o nó com a nova carteira", + "switch_progress_stopping": "Parando o nó atual", + "switch_progress_title": "Trocando de carteira", "switch_stopnode_body": "Trocar de carteira reinicia o nó com a carteira selecionada. O nó em execução será parado e reiniciado com a nova carteira — se você o deixou em execução de propósito, ele volta automaticamente.", "switch_stopnode_confirm": "Parar nó e trocar", "switch_stopnode_title": "Parar o nó em execução?", diff --git a/res/lang/ru.json b/res/lang/ru.json index 8753c90..052884a 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -1294,6 +1294,13 @@ "sweep_to": "Переведено на:", "sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)", "sweep_tx": "Транзакция:", + "switch_progress_background": "Продолжить в фоне", + "switch_progress_failed_title": "Не удалось переключить кошелёк", + "switch_progress_hint": "Корректное завершение работы может занять до минуты.", + "switch_progress_reconnecting": "Переподключение", + "switch_progress_starting": "Запуск узла с новым кошельком", + "switch_progress_stopping": "Остановка текущего узла", + "switch_progress_title": "Переключение кошелька", "switch_stopnode_body": "Смена кошелька перезапускает узел с выбранным кошельком. Запущенный узел будет остановлен и перезапущен с новым кошельком — если вы намеренно оставили его работать, он запустится снова автоматически.", "switch_stopnode_confirm": "Остановить узел и переключить", "switch_stopnode_title": "Остановить запущенный узел?", diff --git a/res/lang/zh.json b/res/lang/zh.json index abcb69f..435dc01 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -1294,6 +1294,13 @@ "sweep_to": "归集到:", "sweep_toggle": "归集到我的钱包(不保留密钥)", "sweep_tx": "交易:", + "switch_progress_background": "在后台继续", + "switch_progress_failed_title": "切换钱包失败", + "switch_progress_hint": "正常关闭最多可能需要一分钟。", + "switch_progress_reconnecting": "正在重新连接", + "switch_progress_starting": "正在以新钱包启动节点", + "switch_progress_stopping": "正在停止当前节点", + "switch_progress_title": "正在切换钱包", "switch_stopnode_body": "切换钱包会以所选钱包重启节点。正在运行的节点将被停止并以新钱包重新启动——如果你是特意让它保持运行的,它会自动重新启动。", "switch_stopnode_confirm": "停止节点并切换", "switch_stopnode_title": "停止正在运行的节点?", diff --git a/src/app.cpp b/src/app.cpp index 78489bf..411edb3 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -3863,35 +3863,70 @@ void App::renderAntivirusHelpDialog() void App::renderSwitchStopDaemonDialog() { - if (!show_switch_stop_daemon_confirm_) return; + const bool confirm = show_switch_stop_daemon_confirm_; + const bool progress = wallet_switch_dialog_open_.load(); + if (!confirm && !progress) return; + + const auto phase = static_cast(wallet_switch_phase_.load()); + const bool failed = progress && phase == WalletSwitchPhase::Failed; ui::material::OverlayDialogSpec ov; - ov.title = TR("switch_stopnode_title"); - ov.p_open = &show_switch_stop_daemon_confirm_; // the X / backdrop click acts as Cancel + ov.title = confirm ? TR("switch_stopnode_title") + : failed ? TR("switch_progress_failed_title") + : TR("switch_progress_title"); + // A confirm can be dismissed via X/backdrop (= cancel); a running switch cannot (stay until it's up). + ov.p_open = confirm ? &show_switch_stop_daemon_confirm_ : nullptr; ov.style = ui::material::OverlayStyle::BlurFloat; ov.cardWidth = 520.0f; ov.idSuffix = "switchstopnode"; if (!ui::material::BeginOverlayDialog(ov)) { - // Closed via X/backdrop — treat as cancel: drop the pending target. - if (!show_switch_stop_daemon_confirm_) pending_switch_wallet_file_.clear(); + if (confirm && !show_switch_stop_daemon_confirm_) pending_switch_wallet_file_.clear(); // X = cancel return; } const float dp = ui::Layout::dpiScale(); - ui::material::DialogWarningHeader(TR("switch_stopnode_warn")); - ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); - ImGui::TextWrapped("%s", TR("switch_stopnode_body")); - ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); - - if (ui::material::TactileButton(TR("switch_stopnode_confirm"), ImVec2(200.0f * dp, 0))) { - const std::string w = pending_switch_wallet_file_; - pending_switch_wallet_file_.clear(); - show_switch_stop_daemon_confirm_ = false; - switchToWallet(w, /*stopDaemonConfirmed=*/true); // now proceeds to stop + wait + restart - } - ImGui::SameLine(); - if (ui::material::TactileButton(TR("cancel"), ImVec2(110.0f * dp, 0))) { - pending_switch_wallet_file_.clear(); - show_switch_stop_daemon_confirm_ = false; + if (confirm) { + ui::material::DialogWarningHeader(TR("switch_stopnode_warn")); + ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); + ImGui::TextWrapped("%s", TR("switch_stopnode_body")); + ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); + if (ui::material::TactileButton(TR("switch_stopnode_confirm"), ImVec2(200.0f * dp, 0))) { + const std::string w = pending_switch_wallet_file_; + pending_switch_wallet_file_.clear(); + show_switch_stop_daemon_confirm_ = false; + switchToWallet(w, /*stopDaemonConfirmed=*/true); // opens the live-progress phase below + } + ImGui::SameLine(); + if (ui::material::TactileButton(TR("cancel"), ImVec2(110.0f * dp, 0))) { + pending_switch_wallet_file_.clear(); + show_switch_stop_daemon_confirm_ = false; + } + } else if (failed) { + ui::material::DialogWarningHeader(TR("switch_progress_failed_title")); + ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); + ImGui::TextWrapped("%s", wallet_switch_error_.c_str()); + ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); + if (ui::material::TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) { + wallet_switch_dialog_open_.store(false); + wallet_switch_phase_.store(static_cast(WalletSwitchPhase::None)); + wallet_switch_error_.clear(); + } + } else { + // Live progress: an animated phase line, kept redrawing by isWalletSwitchInProgress(). + const char* status = + phase == WalletSwitchPhase::Starting ? TR("switch_progress_starting") + : phase == WalletSwitchPhase::Reconnecting ? TR("switch_progress_reconnecting") + : TR("switch_progress_stopping"); + ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); + ui::material::Type().text(ui::material::TypeStyle::Subtitle1, + (std::string(status) + ui::material::LoadingDots()).c_str()); + if (phase == WalletSwitchPhase::Stopping) { + ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); + ImGui::TextWrapped("%s", TR("switch_progress_hint")); + } + ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); + // Escape hatch — the switch keeps running in the background (a toast reports the result). + if (ui::material::TactileButton(TR("switch_progress_background"), ImVec2(220.0f * dp, 0))) + wallet_switch_dialog_open_.store(false); } ui::material::EndOverlayDialog(); diff --git a/src/app.h b/src/app.h index 155542c..304ac32 100644 --- a/src/app.h +++ b/src/app.h @@ -139,6 +139,9 @@ public: * @brief Whether we are in the shutdown phase */ bool isShuttingDown() const { return shutting_down_; } + // True while the wallet-switch progress modal is open — keeps the frame loop redrawing so its live + // phase/spinner update in real time even when the app is otherwise idle. + bool isWalletSwitchInProgress() const { return wallet_switch_dialog_open_.load(); } wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); } bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); } bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); } @@ -768,6 +771,14 @@ private: // sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true). bool show_switch_stop_daemon_confirm_ = false; std::string pending_switch_wallet_file_; + // Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start → + // reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated; + // dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread. + // error_ is written only on the main thread (processWalletSwitchRevert) and read by the (main-thread) UI. + enum class WalletSwitchPhase : int { None = 0, Stopping, Starting, Reconnecting, Failed }; + std::atomic wallet_switch_phase_{0}; + std::atomic wallet_switch_dialog_open_{false}; + std::string wallet_switch_error_; // 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. diff --git a/src/app_network.cpp b/src/app_network.cpp index 3edc525..3e4bdef 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -504,7 +504,13 @@ void App::tryConnect() void App::onConnected() { state_.connected = true; - wallet_switch_pending_confirm_.store(false); // a successful connect confirms the switch + const bool completedSwitch = wallet_switch_pending_confirm_.exchange(false); // this connect confirms a switch + // A successful connect completes a switch — close/clear the progress modal (kept open through the + // stop → start → reconnect sequence, or already hidden via "continue in background"). + if (completedSwitch || wallet_switch_dialog_open_.load()) { + wallet_switch_dialog_open_.store(false); + wallet_switch_phase_.store(static_cast(WalletSwitchPhase::None)); + } state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications daemon_start_error_shown_ = false; @@ -1104,9 +1110,11 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed // can't surface (or sign outgoing chat) under the new wallet. It re-provisions from the new // wallet's seed once it connects. onDisconnected alone doesn't cover chat. resetChatSession(); - ui::Notifications::instance().info( - "Switching wallet — stopping the node and restarting on the new wallet. " - "A graceful shutdown can take up to a minute.", 60.0f); + // Open the switch progress modal — it stays up through stop → wait-for-exit → start → reconnect and + // auto-closes when the new node connects (onConnected). The worker advances the phase from here. + wallet_switch_error_.clear(); + wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Stopping)); + wallet_switch_dialog_open_.store(true); async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) { bool ok = false; @@ -1126,6 +1134,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed } else { // The relaunch is ours — clear the adopted-external latch so the fresh process is treated // as owned (stop/isRunning/exit behave normally for the next switch and app exit). + wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Starting)); if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); ok = shutting_down_ ? true : startEmbeddedDaemon(); @@ -1135,6 +1144,8 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed std::this_thread::sleep_for(std::chrono::milliseconds(1500)); if (!isEmbeddedDaemonRunning()) ok = false; } + // Process is up and staying up — now waiting for RPC to answer (onConnected closes the modal). + if (ok) wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Reconnecting)); } } catch (...) { ok = false; // a throw during stop/start is a failed switch — revert rather than wedge @@ -1167,12 +1178,17 @@ void App::processWalletSwitchRevert() if (daemon_controller_) daemon_controller_->resetCrashCount(); // Accurate reason: "port wouldn't free" (the running node kept the connection) vs. a genuinely bad // target wallet. The former is the adopted-external-daemon case; the latter a daemon that exited in init. - if (switch_stop_failed_.exchange(false)) { - ui::Notifications::instance().error( - "Couldn't switch wallets — the running node didn't release its connection in time. " - "It's still on the previous wallet."); + const std::string reason = switch_stop_failed_.exchange(false) + ? std::string("Couldn't switch wallets — the running node didn't release its connection in time. " + "It's still on the previous wallet.") + : std::string("Couldn't open that wallet — reverted to the previous one."); + // Surface it in the progress modal if it's still up (keep it open on Failed until the user closes); + // otherwise (the user chose "continue in background") fall back to a toast. + if (wallet_switch_dialog_open_.load()) { + wallet_switch_error_ = reason; + wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Failed)); } else { - ui::Notifications::instance().error("Couldn't open that wallet — reverted to the previous one."); + ui::Notifications::instance().error(reason); } daemon_restarting_ = false; // the connect loop now restarts the previous wallet's daemon } diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index 9d977c7..cb6407a 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -379,6 +379,11 @@ void App::buildSweepCatalog() add("modal-switch-stopnode", ui::NavPage::Settings, [](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; }, [](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); }); + add("modal-switch-progress", ui::NavPage::Settings, + [](App& a) { a.wallet_switch_phase_.store(static_cast(App::WalletSwitchPhase::Stopping)); + a.wallet_switch_dialog_open_.store(true); }, + [](App& a) { a.wallet_switch_dialog_open_.store(false); + a.wallet_switch_phase_.store(static_cast(App::WalletSwitchPhase::None)); }); // Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked). add("modal-shield", ui::NavPage::Send, [](App&) { ui::ShieldDialog::showShieldCoinbase(); }, diff --git a/src/main.cpp b/src/main.cpp index e9a3070..8bb352a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1966,6 +1966,7 @@ int main(int argc, char* argv[]) bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0) && !opaqueBackground; bool animating = app.isShuttingDown() + || app.isWalletSwitchInProgress() || backdropNeedsFrames || app.hasTransactionSendProgress() || app.isTransactionRefreshInProgress() diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index b353004..d933129 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1045,6 +1045,14 @@ void I18n::loadBuiltinEnglish() strings_["switch_stopnode_warn"] = "A node is already running that this wallet didn't start."; strings_["switch_stopnode_body"] = "Switching wallets restarts the node on the wallet you selected. The running node will be stopped and relaunched on the new wallet — if you kept it running on purpose, it comes back up automatically."; strings_["switch_stopnode_confirm"] = "Stop node & switch"; + // Wallet-switch live progress modal + strings_["switch_progress_title"] = "Switching wallet"; + strings_["switch_progress_failed_title"] = "Wallet switch failed"; + strings_["switch_progress_stopping"] = "Stopping the current node"; + strings_["switch_progress_starting"] = "Starting the node on the new wallet"; + strings_["switch_progress_reconnecting"] = "Reconnecting"; + strings_["switch_progress_hint"] = "A graceful shutdown can take up to a minute."; + strings_["switch_progress_background"] = "Continue in background"; // Receive Tab strings_["receiving_addresses"] = "Your Receiving Addresses";