v1.2.0: UX audit — security fixes, accessibility, and polish
Security (P0): - Fix sidebar remaining interactive behind lock screen - Extend auto-lock idle detection to include active widget interactions - Distinguish missing PIN vault from wrong PIN; auto-switch to passphrase Blocking UX (P1): - Add 15s timeout for encryption state check to prevent indefinite loading - Show restart reason in loading overlay after wallet encryption - Add Force Quit button on shutdown screen after 10s - Warn user if embedded daemon fails to start during wizard completion Polish (P2): - Use configured explorer URL in Receive tab instead of hardcoded URL - Increase request memo buffer from 256 to 512 bytes to match Send tab - Extend notification duration to 5s for critical operations (tx sent, wallet encrypted, key import, backup, export) - Add Reduce Motion accessibility setting (disables page fade + balance lerp) - Show estimated remaining time during mining thread benchmark - Add staleness indicator to market price data (warning after 5 min) New i18n keys: incorrect_pin, incorrect_passphrase, pin_not_set, restarting_after_encryption, force_quit, reduce_motion, tt_reduce_motion, ago, wizard_daemon_start_failed
This commit is contained in:
@@ -15,7 +15,7 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.1.2
|
VERSION 1.2.0
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
|
|||||||
48
src/app.cpp
48
src/app.cpp
@@ -316,7 +316,8 @@ void App::update()
|
|||||||
// Track user interaction for auto-lock
|
// Track user interaction for auto-lock
|
||||||
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
||||||
io.MouseClicked[0] || io.MouseClicked[1] ||
|
io.MouseClicked[0] || io.MouseClicked[1] ||
|
||||||
io.InputQueueCharacters.Size > 0) {
|
io.InputQueueCharacters.Size > 0 ||
|
||||||
|
ImGui::IsAnyItemActive()) {
|
||||||
last_interaction_ = std::chrono::steady_clock::now();
|
last_interaction_ = std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,7 +1025,8 @@ void App::render()
|
|||||||
bool prevCollapsed = sidebar_collapsed_;
|
bool prevCollapsed = sidebar_collapsed_;
|
||||||
{
|
{
|
||||||
PERF_SCOPE("Render.Sidebar");
|
PERF_SCOPE("Render.Sidebar");
|
||||||
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_);
|
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_,
|
||||||
|
state_.isLocked());
|
||||||
}
|
}
|
||||||
if (sbStatus.exitClicked) {
|
if (sbStatus.exitClicked) {
|
||||||
requestQuit();
|
requestQuit();
|
||||||
@@ -1039,7 +1041,7 @@ void App::render()
|
|||||||
|
|
||||||
// Page transition: detect change, ramp alpha
|
// Page transition: detect change, ramp alpha
|
||||||
if (current_page_ != prev_page_) {
|
if (current_page_ != prev_page_) {
|
||||||
page_alpha_ = ui::effects::isLowSpecMode() ? 1.0f : 0.0f;
|
page_alpha_ = (ui::effects::isLowSpecMode() || (settings_ && settings_->getReduceMotion())) ? 1.0f : 0.0f;
|
||||||
prev_page_ = current_page_;
|
prev_page_ = current_page_;
|
||||||
}
|
}
|
||||||
if (page_alpha_ < 1.0f) {
|
if (page_alpha_ < 1.0f) {
|
||||||
@@ -1094,6 +1096,18 @@ void App::render()
|
|||||||
// Lock screen — covers tab content just like the loading overlay
|
// Lock screen — covers tab content just like the loading overlay
|
||||||
renderLockScreen();
|
renderLockScreen();
|
||||||
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
|
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
|
||||||
|
// Track how long we've been waiting for encryption state
|
||||||
|
if (state_.connected && !state_.encryption_state_known) {
|
||||||
|
encryption_check_timer_ += ImGui::GetIO().DeltaTime;
|
||||||
|
if (encryption_check_timer_ >= 15.0f) {
|
||||||
|
DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n");
|
||||||
|
state_.encryption_state_known = true;
|
||||||
|
state_.encrypted = false;
|
||||||
|
encryption_check_timer_ = 0.0f;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
encryption_check_timer_ = 0.0f;
|
||||||
|
}
|
||||||
// Reset lock screen focus flag so it auto-focuses next time
|
// Reset lock screen focus flag so it auto-focuses next time
|
||||||
lock_screen_was_visible_ = false;
|
lock_screen_was_visible_ = false;
|
||||||
// Show loading overlay instead of tab content
|
// Show loading overlay instead of tab content
|
||||||
@@ -2403,11 +2417,14 @@ void App::renderShutdownScreen()
|
|||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f));
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||||
ImGui::Begin("##ShutdownOverlay", nullptr,
|
ImGuiWindowFlags shutdownFlags =
|
||||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
|
ImGuiWindowFlags_NoSavedSettings;
|
||||||
ImGuiWindowFlags_NoSavedSettings);
|
// Allow input after 10s so Force Quit button is clickable
|
||||||
|
if (shutdown_timer_ < 10.0f)
|
||||||
|
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
||||||
|
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
||||||
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
// Convert local centre to screen coords for draw-list primitives
|
// Convert local centre to screen coords for draw-list primitives
|
||||||
@@ -2505,6 +2522,25 @@ void App::renderShutdownScreen()
|
|||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// 4b. Force Quit button — appears after 10 seconds
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
if (shutdown_timer_ >= 10.0f) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Spacing();
|
||||||
|
const char* forceLabel = TR("force_quit");
|
||||||
|
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
||||||
|
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
||||||
|
if (ImGui::Button(forceLabel, btnSize)) {
|
||||||
|
DEBUG_LOGF("Force quit requested by user after %.0fs\n", shutdown_timer_);
|
||||||
|
shutdown_complete_ = true;
|
||||||
|
}
|
||||||
|
ImGui::PopStyleColor(3);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
|||||||
@@ -378,6 +378,9 @@ private:
|
|||||||
std::atomic<bool> daemon_restarting_{false};
|
std::atomic<bool> daemon_restarting_{false};
|
||||||
std::thread daemon_restart_thread_;
|
std::thread daemon_restart_thread_;
|
||||||
|
|
||||||
|
// Encryption state check timeout
|
||||||
|
float encryption_check_timer_ = 0.0f;
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
bool quit_requested_ = false;
|
bool quit_requested_ = false;
|
||||||
bool show_demo_window_ = false;
|
bool show_demo_window_ = false;
|
||||||
|
|||||||
@@ -1186,6 +1186,7 @@ void App::refreshPrice()
|
|||||||
state_.market.volume_24h = market.volume_24h;
|
state_.market.volume_24h = market.volume_24h;
|
||||||
state_.market.market_cap = market.market_cap;
|
state_.market.market_cap = market.market_cap;
|
||||||
state_.market.last_updated = market.last_updated;
|
state_.market.last_updated = market.last_updated;
|
||||||
|
state_.market.last_fetch_time = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
state_.market.price_history.push_back(market.price_usd);
|
state_.market.price_history.push_back(market.price_usd);
|
||||||
if (state_.market.price_history.size() > MarketInfo::MAX_HISTORY) {
|
if (state_.market.price_history.size() > MarketInfo::MAX_HISTORY) {
|
||||||
|
|||||||
@@ -69,10 +69,13 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui::Notifications::instance().info(
|
ui::Notifications::instance().info(
|
||||||
"Wallet encrypted successfully");
|
"Wallet encrypted successfully", 5.0f);
|
||||||
|
|
||||||
// The daemon shuts itself down after encryptwallet
|
// The daemon shuts itself down after encryptwallet.
|
||||||
|
// Update connection_status_ so the loading overlay
|
||||||
|
// explains why the daemon is restarting.
|
||||||
if (isUsingEmbeddedDaemon()) {
|
if (isUsingEmbeddedDaemon()) {
|
||||||
|
connection_status_ = TR("restarting_after_encryption");
|
||||||
// Give daemon a moment to shut down, then restart
|
// Give daemon a moment to shut down, then restart
|
||||||
// (do this off the main thread to avoid stalling the UI)
|
// (do this off the main thread to avoid stalling the UI)
|
||||||
std::thread([this]() {
|
std::thread([this]() {
|
||||||
@@ -164,13 +167,13 @@ void App::processDeferredEncryption() {
|
|||||||
if (pinStored) {
|
if (pinStored) {
|
||||||
settings_->setPinEnabled(true);
|
settings_->setPinEnabled(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
ui::Notifications::instance().info("Wallet encrypted & PIN set");
|
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
|
||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
"Wallet encrypted but PIN vault failed");
|
"Wallet encrypted but PIN vault failed");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().info("Wallet encrypted successfully");
|
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Securely clear deferred state
|
// Securely clear deferred state
|
||||||
@@ -264,7 +267,7 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
|
|||||||
state_.unlocked_until = std::time(nullptr) + timeout;
|
state_.unlocked_until = std::time(nullptr) + timeout;
|
||||||
} else {
|
} else {
|
||||||
lock_attempts_++;
|
lock_attempts_++;
|
||||||
lock_error_msg_ = "Incorrect passphrase";
|
lock_error_msg_ = TR("incorrect_passphrase");
|
||||||
lock_error_timer_ = 3.0f;
|
lock_error_timer_ = 3.0f;
|
||||||
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
||||||
|
|
||||||
@@ -788,10 +791,17 @@ void App::renderLockScreen() {
|
|||||||
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
|
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
|
||||||
|
|
||||||
if (!vaultOk) {
|
if (!vaultOk) {
|
||||||
return [this]() {
|
bool noVault = !vault_ || !vault_->hasVault();
|
||||||
|
return [this, noVault]() {
|
||||||
lock_unlock_in_progress_ = false;
|
lock_unlock_in_progress_ = false;
|
||||||
lock_attempts_++;
|
if (noVault) {
|
||||||
lock_error_msg_ = "Incorrect PIN";
|
// Vault file missing — switch to passphrase mode
|
||||||
|
lock_error_msg_ = TR("pin_not_set");
|
||||||
|
lock_use_pin_ = false;
|
||||||
|
} else {
|
||||||
|
lock_attempts_++;
|
||||||
|
lock_error_msg_ = TR("incorrect_pin");
|
||||||
|
}
|
||||||
lock_error_timer_ = 3.0f;
|
lock_error_timer_ = 3.0f;
|
||||||
|
|
||||||
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
||||||
|
|||||||
@@ -1294,7 +1294,10 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
// Start daemon + finish wizard immediately
|
// Start daemon + finish wizard immediately
|
||||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||||
startEmbeddedDaemon();
|
if (!startEmbeddedDaemon()) {
|
||||||
|
ui::Notifications::instance().warning(
|
||||||
|
TR("wizard_daemon_start_failed"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tryConnect();
|
tryConnect();
|
||||||
wizard_phase_ = WizardPhase::Done;
|
wizard_phase_ = WizardPhase::Done;
|
||||||
@@ -1313,7 +1316,10 @@ void App::renderFirstRunWizard() {
|
|||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||||
startEmbeddedDaemon();
|
if (!startEmbeddedDaemon()) {
|
||||||
|
ui::Notifications::instance().warning(
|
||||||
|
TR("wizard_daemon_start_failed"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tryConnect();
|
tryConnect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ bool Settings::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
|
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
|
||||||
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
||||||
|
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
|
||||||
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
||||||
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
||||||
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
||||||
@@ -240,6 +241,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["debug_categories"].push_back(cat);
|
j["debug_categories"].push_back(cat);
|
||||||
j["theme_effects_enabled"] = theme_effects_enabled_;
|
j["theme_effects_enabled"] = theme_effects_enabled_;
|
||||||
j["low_spec_mode"] = low_spec_mode_;
|
j["low_spec_mode"] = low_spec_mode_;
|
||||||
|
j["reduce_motion"] = reduce_motion_;
|
||||||
j["selected_exchange"] = selected_exchange_;
|
j["selected_exchange"] = selected_exchange_;
|
||||||
j["selected_pair"] = selected_pair_;
|
j["selected_pair"] = selected_pair_;
|
||||||
j["pool_url"] = pool_url_;
|
j["pool_url"] = pool_url_;
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ public:
|
|||||||
bool getLowSpecMode() const { return low_spec_mode_; }
|
bool getLowSpecMode() const { return low_spec_mode_; }
|
||||||
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
|
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
|
||||||
|
|
||||||
|
// Reduce motion — disables animated transitions for accessibility
|
||||||
|
bool getReduceMotion() const { return reduce_motion_; }
|
||||||
|
void setReduceMotion(bool v) { reduce_motion_ = v; }
|
||||||
|
|
||||||
// Market — last selected exchange + pair
|
// Market — last selected exchange + pair
|
||||||
std::string getSelectedExchange() const { return selected_exchange_; }
|
std::string getSelectedExchange() const { return selected_exchange_; }
|
||||||
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
||||||
@@ -303,6 +307,7 @@ private:
|
|||||||
std::set<std::string> debug_categories_;
|
std::set<std::string> debug_categories_;
|
||||||
bool theme_effects_enabled_ = true;
|
bool theme_effects_enabled_ = true;
|
||||||
bool low_spec_mode_ = false;
|
bool low_spec_mode_ = false;
|
||||||
|
bool reduce_motion_ = false;
|
||||||
std::string selected_exchange_ = "TradeOgre";
|
std::string selected_exchange_ = "TradeOgre";
|
||||||
std::string selected_pair_ = "DRGX/BTC";
|
std::string selected_pair_ = "DRGX/BTC";
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
||||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
||||||
|
|
||||||
#define DRAGONX_VERSION "1.1.2"
|
#define DRAGONX_VERSION "1.2.0"
|
||||||
#define DRAGONX_VERSION_MAJOR 1
|
#define DRAGONX_VERSION_MAJOR 1
|
||||||
#define DRAGONX_VERSION_MINOR 1
|
#define DRAGONX_VERSION_MINOR 2
|
||||||
#define DRAGONX_VERSION_PATCH 2
|
#define DRAGONX_VERSION_PATCH 0
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||||
#define DRAGONX_ORG_NAME "Hush"
|
#define DRAGONX_ORG_NAME "Hush"
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ struct MarketInfo {
|
|||||||
double change_24h = 0.0;
|
double change_24h = 0.0;
|
||||||
double market_cap = 0.0;
|
double market_cap = 0.0;
|
||||||
std::string last_updated;
|
std::string last_updated;
|
||||||
|
std::chrono::steady_clock::time_point last_fetch_time{};
|
||||||
|
|
||||||
// Price history for chart
|
// Price history for chart
|
||||||
std::vector<double> price_history;
|
std::vector<double> price_history;
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ static bool sp_gradient_background = false;
|
|||||||
// Low-spec mode
|
// Low-spec mode
|
||||||
static bool sp_low_spec_mode = false;
|
static bool sp_low_spec_mode = false;
|
||||||
|
|
||||||
|
// Reduce motion (accessibility)
|
||||||
|
static bool sp_reduce_motion = false;
|
||||||
|
|
||||||
// Font scale (user accessibility, 1.0–3.0)
|
// Font scale (user accessibility, 1.0–3.0)
|
||||||
static float sp_font_scale = 1.0f;
|
static float sp_font_scale = 1.0f;
|
||||||
|
|
||||||
@@ -179,6 +182,7 @@ static void loadSettingsPageState(config::Settings* settings) {
|
|||||||
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
|
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
|
||||||
sp_low_spec_mode = settings->getLowSpecMode();
|
sp_low_spec_mode = settings->getLowSpecMode();
|
||||||
effects::setLowSpecMode(sp_low_spec_mode);
|
effects::setLowSpecMode(sp_low_spec_mode);
|
||||||
|
sp_reduce_motion = settings->getReduceMotion();
|
||||||
sp_font_scale = settings->getFontScale();
|
sp_font_scale = settings->getFontScale();
|
||||||
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
|
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
|
||||||
sp_keep_daemon_running = settings->getKeepDaemonRunning();
|
sp_keep_daemon_running = settings->getKeepDaemonRunning();
|
||||||
@@ -232,6 +236,7 @@ static void saveSettingsPageState(config::Settings* settings) {
|
|||||||
settings->setScanlineEnabled(sp_scanline_enabled);
|
settings->setScanlineEnabled(sp_scanline_enabled);
|
||||||
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
|
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
|
||||||
settings->setLowSpecMode(sp_low_spec_mode);
|
settings->setLowSpecMode(sp_low_spec_mode);
|
||||||
|
settings->setReduceMotion(sp_reduce_motion);
|
||||||
settings->setFontScale(sp_font_scale);
|
settings->setFontScale(sp_font_scale);
|
||||||
settings->setKeepDaemonRunning(sp_keep_daemon_running);
|
settings->setKeepDaemonRunning(sp_keep_daemon_running);
|
||||||
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
||||||
@@ -672,6 +677,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
|
||||||
|
|
||||||
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
|
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) {
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
|
||||||
|
|
||||||
ImGui::BeginDisabled(sp_low_spec_mode);
|
ImGui::BeginDisabled(sp_low_spec_mode);
|
||||||
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
@@ -962,6 +973,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
|
||||||
|
|
||||||
|
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) {
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
|
||||||
|
|
||||||
ImGui::BeginDisabled(sp_low_spec_mode);
|
ImGui::BeginDisabled(sp_low_spec_mode);
|
||||||
|
|
||||||
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
|
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|||||||
// collapsed: when true, sidebar is in icon-only mode (narrow width).
|
// collapsed: when true, sidebar is in icon-only mode (narrow width).
|
||||||
// The caller can toggle collapsed via a reference if a toggle button is desired.
|
// The caller can toggle collapsed via a reference if a toggle button is desired.
|
||||||
inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight,
|
inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight,
|
||||||
SidebarStatus& status, bool& collapsed)
|
SidebarStatus& status, bool& collapsed, bool locked = false)
|
||||||
{
|
{
|
||||||
using namespace material;
|
using namespace material;
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
@@ -610,8 +610,12 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click detection
|
// Click detection — block pages that require unlock when locked
|
||||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
bool pageNeedsUnlock = locked &&
|
||||||
|
item.page != NavPage::Console &&
|
||||||
|
item.page != NavPage::Peers &&
|
||||||
|
item.page != NavPage::Settings;
|
||||||
|
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
|
||||||
current = item.page;
|
current = item.page;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
@@ -620,7 +624,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
float iconS = iconHalfSize; // icon half-size in pixels
|
float iconS = iconHalfSize; // icon half-size in pixels
|
||||||
float iconCY = cursor.y + btnH * 0.5f;
|
float iconCY = cursor.y + btnH * 0.5f;
|
||||||
float textY = cursor.y + (btnH - ImGui::GetTextLineHeight()) * 0.5f;
|
float textY = cursor.y + (btnH - ImGui::GetTextLineHeight()) * 0.5f;
|
||||||
ImU32 textCol = selected ? Primary() : OnSurfaceMedium();
|
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
|
||||||
|
|
||||||
if (showLabels) {
|
if (showLabels) {
|
||||||
// Measure total width of icon + gap + label, then center.
|
// Measure total width of icon + gap + label, then center.
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ void BackupWalletDialog::render(App* app)
|
|||||||
s_status = statusMsg;
|
s_status = statusMsg;
|
||||||
s_backing_up = false;
|
s_backing_up = false;
|
||||||
if (success) {
|
if (success) {
|
||||||
Notifications::instance().success(TR("backup_created"));
|
Notifications::instance().success(TR("backup_created"), 5.0f);
|
||||||
} else {
|
} else {
|
||||||
Notifications::instance().warning(statusMsg);
|
Notifications::instance().warning(statusMsg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
// Lerp displayed balances toward actual values
|
// Lerp displayed balances toward actual values
|
||||||
{
|
{
|
||||||
float dt = ImGui::GetIO().DeltaTime;
|
float dt = ImGui::GetIO().DeltaTime;
|
||||||
float speed = kBalanceLerpSpeed;
|
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
|
||||||
auto lerp = [](double& disp, double target, float dt, float spd) {
|
auto lerp = [](double& disp, double target, float dt, float spd) {
|
||||||
double diff = target - disp;
|
double diff = target - disp;
|
||||||
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
||||||
@@ -1278,7 +1278,7 @@ static void UpdateBalanceLerp(App* app) {
|
|||||||
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
||||||
const auto& state = app->state();
|
const auto& state = app->state();
|
||||||
float dt = ImGui::GetIO().DeltaTime;
|
float dt = ImGui::GetIO().DeltaTime;
|
||||||
float speed = kBalanceLerpSpeed;
|
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
|
||||||
auto lerp = [](double& disp, double target, float dt, float spd) {
|
auto lerp = [](double& disp, double target, float dt, float spd) {
|
||||||
double diff = target - disp;
|
double diff = target - disp;
|
||||||
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ void ExportAllKeysDialog::render(App* app)
|
|||||||
s_exporting = false;
|
s_exporting = false;
|
||||||
if (writeOk) {
|
if (writeOk) {
|
||||||
s_status = "Exported to: " + filepath;
|
s_status = "Exported to: " + filepath;
|
||||||
Notifications::instance().success(TR("export_keys_success"));
|
Notifications::instance().success(TR("export_keys_success"), 5.0f);
|
||||||
} else {
|
} else {
|
||||||
s_status = "Failed to write file";
|
s_status = "Failed to write file";
|
||||||
Notifications::instance().error("Failed to save key file");
|
Notifications::instance().error("Failed to save key file");
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
|
|
||||||
s_status = "Exported " + std::to_string(state.transactions.size()) +
|
s_status = "Exported " + std::to_string(state.transactions.size()) +
|
||||||
" transactions to: " + filepath;
|
" transactions to: " + filepath;
|
||||||
Notifications::instance().success(TR("export_tx_success"));
|
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ void ImportKeyDialog::render(App* app)
|
|||||||
imported, failed);
|
imported, failed);
|
||||||
s_status = buf;
|
s_status = buf;
|
||||||
if (imported > 0) {
|
if (imported > 0) {
|
||||||
Notifications::instance().success(TR("import_key_success"));
|
Notifications::instance().success(TR("import_key_success"), 5.0f);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -237,6 +237,26 @@ void RenderMarketTab(App* app)
|
|||||||
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
|
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- STALENESS INDICATOR ----
|
||||||
|
{
|
||||||
|
auto fetchTime = market.last_fetch_time;
|
||||||
|
if (fetchTime.time_since_epoch().count() > 0) {
|
||||||
|
auto elapsed = std::chrono::steady_clock::now() - fetchTime;
|
||||||
|
int ageSecs = (int)std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
|
||||||
|
bool stale = ageSecs > 300; // 5 minutes
|
||||||
|
if (ageSecs < 60)
|
||||||
|
snprintf(buf, sizeof(buf), "%s %ds %s", stale ? ICON_MD_WARNING : "", ageSecs, TR("ago"));
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "%s %dm %s", stale ? ICON_MD_WARNING : "", ageSecs / 60, TR("ago"));
|
||||||
|
ImFont* staleFont = capFont;
|
||||||
|
ImU32 staleCol = stale ? Warning() : WithAlpha(OnSurface(), 100);
|
||||||
|
float staleY = statsY + ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm();
|
||||||
|
ImVec2 staleSz = staleFont->CalcTextSizeA(staleFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
|
dl->AddText(staleFont, staleFont->LegacySize,
|
||||||
|
ImVec2(cardMin.x + Layout::spacingLg(), staleY), staleCol, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- TRADE BUTTON (top-right of card) ----
|
// ---- TRADE BUTTON (top-right of card) ----
|
||||||
if (!currentExchange.pairs.empty()) {
|
if (!currentExchange.pairs.empty()) {
|
||||||
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
|
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
|
||||||
|
|||||||
@@ -1297,10 +1297,28 @@ static void RenderMiningTabContent(App* app)
|
|||||||
// Status text above bar
|
// Status text above bar
|
||||||
int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size()
|
int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size()
|
||||||
? s_benchmark.candidates[s_benchmark.current_index] : 0;
|
? s_benchmark.candidates[s_benchmark.current_index] : 0;
|
||||||
snprintf(buf, sizeof(buf), "%s %d/%d (%dt)",
|
// Estimated remaining time
|
||||||
TR("mining_benchmark_testing"),
|
int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index;
|
||||||
s_benchmark.current_index + 1,
|
float elapsed_in_phase = s_benchmark.phase_timer;
|
||||||
(int)s_benchmark.candidates.size(), ct);
|
float phase_total = (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp)
|
||||||
|
? ThreadBenchmark::WARMUP_SECS
|
||||||
|
: ThreadBenchmark::MEASURE_SECS;
|
||||||
|
float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase);
|
||||||
|
// Remaining tests after current each need warmup + measure
|
||||||
|
float est_secs = remaining_in_current
|
||||||
|
+ (remaining_tests - 1) * (ThreadBenchmark::WARMUP_SECS + ThreadBenchmark::MEASURE_SECS);
|
||||||
|
int est_min = (int)(est_secs / 60.0f);
|
||||||
|
int est_sec = (int)est_secs % 60;
|
||||||
|
if (est_min > 0)
|
||||||
|
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds",
|
||||||
|
TR("mining_benchmark_testing"),
|
||||||
|
s_benchmark.current_index + 1,
|
||||||
|
(int)s_benchmark.candidates.size(), ct, est_min, est_sec);
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds",
|
||||||
|
TR("mining_benchmark_testing"),
|
||||||
|
s_benchmark.current_index + 1,
|
||||||
|
(int)s_benchmark.candidates.size(), ct, est_sec);
|
||||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp),
|
ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp),
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
#include "receive_tab.h"
|
#include "receive_tab.h"
|
||||||
#include "send_tab.h"
|
#include "send_tab.h"
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/platform.h"
|
||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
#include "../../data/wallet_state.h"
|
#include "../../data/wallet_state.h"
|
||||||
#include "../../ui/widgets/qr_code.h"
|
#include "../../ui/widgets/qr_code.h"
|
||||||
@@ -45,7 +47,7 @@ static std::string TrId(const char* key, const char* id) {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
static int s_selected_address_idx = -1;
|
static int s_selected_address_idx = -1;
|
||||||
static double s_request_amount = 0.0;
|
static double s_request_amount = 0.0;
|
||||||
static char s_request_memo[256] = "";
|
static char s_request_memo[512] = "";
|
||||||
static double s_request_usd_amount = 0.0;
|
static double s_request_usd_amount = 0.0;
|
||||||
static bool s_request_usd_mode = false;
|
static bool s_request_usd_mode = false;
|
||||||
static std::string s_cached_qr_data;
|
static std::string s_cached_qr_data;
|
||||||
@@ -76,16 +78,9 @@ static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40)
|
|||||||
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
|
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void OpenExplorerURL(const std::string& address) {
|
static void OpenExplorerURL(App* app, const std::string& address) {
|
||||||
std::string url = "https://explorer.dragonx.com/address/" + address;
|
std::string url = app->settings()->getAddressExplorerUrl() + address;
|
||||||
#ifdef _WIN32
|
dragonx::util::Platform::openUrl(url);
|
||||||
std::string cmd = "start \"\" \"" + url + "\"";
|
|
||||||
#elif __APPLE__
|
|
||||||
std::string cmd = "open \"" + url + "\"";
|
|
||||||
#else
|
|
||||||
std::string cmd = "xdg-open \"" + url + "\"";
|
|
||||||
#endif
|
|
||||||
system(cmd.c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -978,7 +973,7 @@ void RenderReceiveTab(App* app)
|
|||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
||||||
if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||||
OpenExplorerURL(selected.address);
|
OpenExplorerURL(app, selected.address);
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar(); // FrameBorderSize
|
ImGui::PopStyleVar(); // FrameBorderSize
|
||||||
ImGui::PopStyleColor(4);
|
ImGui::PopStyleColor(4);
|
||||||
|
|||||||
@@ -804,7 +804,7 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
s_tx_status = TR("send_tx_sent");
|
s_tx_status = TR("send_tx_sent");
|
||||||
s_result_txid = result;
|
s_result_txid = result;
|
||||||
s_status_success = true;
|
s_status_success = true;
|
||||||
Notifications::instance().success(TR("send_tx_success"));
|
Notifications::instance().success(TR("send_tx_success"), 5.0f);
|
||||||
s_to_address[0] = '\0';
|
s_to_address[0] = '\0';
|
||||||
s_amount = 0.0;
|
s_amount = 0.0;
|
||||||
s_memo[0] = '\0';
|
s_memo[0] = '\0';
|
||||||
|
|||||||
@@ -256,6 +256,15 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["settings_quick_unlock_pin"] = "Quick-unlock PIN";
|
strings_["settings_quick_unlock_pin"] = "Quick-unlock PIN";
|
||||||
strings_["settings_pin_active"] = "PIN";
|
strings_["settings_pin_active"] = "PIN";
|
||||||
strings_["settings_encrypt_first_pin"] = "Encrypt wallet first to enable PIN";
|
strings_["settings_encrypt_first_pin"] = "Encrypt wallet first to enable PIN";
|
||||||
|
strings_["incorrect_pin"] = "Incorrect PIN";
|
||||||
|
strings_["incorrect_passphrase"] = "Incorrect passphrase";
|
||||||
|
strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock.";
|
||||||
|
strings_["restarting_after_encryption"] = "Restarting daemon after encryption...";
|
||||||
|
strings_["force_quit"] = "Force Quit";
|
||||||
|
strings_["reduce_motion"] = "Reduce Motion";
|
||||||
|
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
|
||||||
|
strings_["ago"] = "ago";
|
||||||
|
strings_["wizard_daemon_start_failed"] = "Failed to start daemon \xe2\x80\x94 it will be retried automatically";
|
||||||
strings_["settings_data_dir"] = "Data Dir:";
|
strings_["settings_data_dir"] = "Data Dir:";
|
||||||
strings_["settings_wallet_size_label"] = "Wallet Size:";
|
strings_["settings_wallet_size_label"] = "Wallet Size:";
|
||||||
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
|
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
|
||||||
|
|||||||
Reference in New Issue
Block a user