daemon version check, idle mining control, bootstrap mirror, import key paste, and cleanup
- Add startup binary version checking for dragonxd/xmrig - Display daemon version in UI - Add idle mining thread count adjustment - Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard - Add paste button to import private key dialog with address validation - Add z-address generation UI feedback (loading indicator) - Add option to delete blockchain data while preserving wallet.dat - Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus) - Fix Windows RPC auth: trim \r from config values, add .cookie fallback - Fix connection status message during block index loading - Improve application shutdown to prevent lingering background process
This commit is contained in:
173
.github/copilot-instructions.md
vendored
173
.github/copilot-instructions.md
vendored
@@ -1,173 +0,0 @@
|
||||
# Copilot Instructions — DragonX ImGui Wallet
|
||||
|
||||
## UI Layout: All values in `ui.toml`
|
||||
|
||||
**Every UI layout constant must be defined in `res/themes/ui.toml` and read at runtime via the schema API.** Never hardcode pixel sizes, ratios, rounding values, thicknesses, or spacing constants directly in C++ source files. This is critical for maintainability, theming support, and hot-reload.
|
||||
|
||||
### Schema API reference
|
||||
|
||||
The singleton is accessed via `schema::UI()` (header: `#include "../schema/ui_schema.h"`).
|
||||
|
||||
| Method | Returns | Use for |
|
||||
|---|---|---|
|
||||
| `drawElement(section, name)` | `DrawElementStyle` | Custom DrawList layout values (`.size`, `.height`, `.thickness`, `.radius`, `.opacity`) |
|
||||
| `button(section, name)` | `ButtonStyle` | Button width/height/font |
|
||||
| `input(section, name)` | `InputStyle` | Input field dimensions |
|
||||
| `label(section, name)` | `LabelStyle` | Label styling |
|
||||
| `table(section, name)` | `TableStyle` | Table layout |
|
||||
| `window(section, name)` | `WindowStyle` | Window/dialog dimensions |
|
||||
| `combo(section, name)` | `ComboStyle` | Combo box styling |
|
||||
| `slider(section, name)` | `SliderStyle` | Slider styling |
|
||||
| `checkbox(section, name)` | `CheckboxStyle` | Checkbox styling |
|
||||
| `separator(section, name)` | `SeparatorStyle` | Separator/divider styling |
|
||||
|
||||
### Section naming convention
|
||||
|
||||
Sections use dot-separated paths matching the file/feature:
|
||||
|
||||
- `tabs.send`, `tabs.receive`, `tabs.transactions`, `tabs.mining`, `tabs.peers`, `tabs.market` — tab-specific values
|
||||
- `tabs.balance` — balance/home tab
|
||||
- `components.main-layout`, `components.settings-page` — shared components
|
||||
- `dialogs.about`, `dialogs.backup`, etc. — dialog-specific values
|
||||
- `sidebar` — navigation sidebar
|
||||
|
||||
### How to add a new layout value
|
||||
|
||||
1. **Add the entry to `res/themes/ui.toml`** under the appropriate section:
|
||||
```toml
|
||||
[tabs.example]
|
||||
my-element = {size = 42.0}
|
||||
```
|
||||
|
||||
2. **Read it in C++** instead of using a literal:
|
||||
```cpp
|
||||
// WRONG — hardcoded
|
||||
float myValue = 42.0f;
|
||||
|
||||
// CORRECT — schema-driven
|
||||
float myValue = schema::UI().drawElement("tabs.example", "my-element").size;
|
||||
```
|
||||
|
||||
3. For values used as min/max pairs with scaling:
|
||||
```cpp
|
||||
// WRONG
|
||||
float h = std::max(18.0f, 24.0f * vScale);
|
||||
|
||||
// CORRECT
|
||||
float h = std::max(
|
||||
schema::UI().drawElement("tabs.example", "row-min-height").size,
|
||||
schema::UI().drawElement("tabs.example", "row-height").size * vScale
|
||||
);
|
||||
```
|
||||
|
||||
### What belongs in `ui.toml`
|
||||
|
||||
- Pixel sizes (card heights, icon sizes, bar widths/heights)
|
||||
- Ratios (column width ratios, max-width ratios)
|
||||
- Rounding values (`FrameRounding`, corner radius)
|
||||
- Thickness values (accent bars, chart lines, borders)
|
||||
- Dot/circle radii
|
||||
- Fade zones, padding constants
|
||||
- Min/max dimension bounds
|
||||
- Font selection (via schema font name strings, resolved with `S.resolveFont()`)
|
||||
- Colors (via `schema::UI().resolveColor()` or color variable references like `"var(--primary)"`)
|
||||
- Animation durations (transition times, fade durations, pulse speeds)
|
||||
- Business logic values (fee amounts, ticker strings, buffer sizes, reward amounts)
|
||||
|
||||
### What does NOT belong in `ui.toml`
|
||||
|
||||
- Spacing that already goes through `Layout::spacing*()` or `spacing::dp()`
|
||||
|
||||
### Legacy system: `UILayout`
|
||||
|
||||
`UILayout::instance()` is the older layout system still used for fonts, typography, panels, and global spacing. New layout values should use `schema::UI().drawElement()` instead. Do not add new keys to `UILayout`.
|
||||
|
||||
### Validation
|
||||
|
||||
After editing `ui.toml`, always validate:
|
||||
```bash
|
||||
python3 -c "import toml; toml.load('res/themes/ui.toml'); print('Valid TOML')"
|
||||
```
|
||||
|
||||
Or with the C++ toml++ parser (which is what the app uses):
|
||||
```bash
|
||||
cd build && make -j$(nproc)
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
cd build && make -j$(nproc)
|
||||
|
||||
# Windows cross-compile
|
||||
./build.sh --win-release
|
||||
```
|
||||
|
||||
## Plans
|
||||
|
||||
When asked to "create a plan", always create a new markdown document in the `docs/` directory with the plan contents.
|
||||
|
||||
## Icons: Use Material Design icon font, never Unicode symbols
|
||||
|
||||
**Never use raw Unicode symbols or emoji characters** (e.g. `↓`, `↗`, `⛏`, `🔍`, `📬`, `⚠️`, `ℹ`) for icons in C++ code. Always use the **Material Design Icons font** via the `ICON_MD_*` defines from `#include "../../embedded/IconsMaterialDesign.h"`.
|
||||
|
||||
### Icon font API
|
||||
|
||||
| Method | Size | Fallback |
|
||||
|---|---|---|
|
||||
| `Type().iconSmall()` | 14px | Body2 |
|
||||
| `Type().iconMed()` | 18px | Body1 |
|
||||
| `Type().iconLarge()` | 24px | H5 |
|
||||
| `Type().iconXL()` | 40px | H3 |
|
||||
|
||||
### Correct usage
|
||||
|
||||
```cpp
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
|
||||
// WRONG — raw Unicode symbol
|
||||
itemSpec.leadingIcon = "↙";
|
||||
|
||||
// CORRECT — Material Design icon codepoint
|
||||
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
|
||||
|
||||
// WRONG — emoji for search
|
||||
searchSpec.leadingIcon = "🔍";
|
||||
|
||||
// CORRECT — Material Design icon
|
||||
searchSpec.leadingIcon = ICON_MD_SEARCH;
|
||||
|
||||
// For rendering with icon font directly:
|
||||
ImGui::PushFont(Type().iconSmall());
|
||||
ImGui::TextUnformatted(ICON_MD_ARROW_DOWNWARD);
|
||||
ImGui::PopFont();
|
||||
```
|
||||
|
||||
### Why
|
||||
|
||||
Raw Unicode symbols and emoji render inconsistently across platforms and may not be present in the loaded text fonts. The Material Design icon font is always loaded and provides consistent, scalable glyphs on both Linux and Windows.
|
||||
|
||||
### Audit for Unicode symbols
|
||||
|
||||
Before completing any task that touches UI code, search for and replace any raw Unicode symbols that may have been introduced. Common symbols to look for:
|
||||
|
||||
| Unicode | Replacement |
|
||||
|---------|-------------|
|
||||
| `▶` `▷` | `ICON_MD_PLAY_ARROW` |
|
||||
| `■` `□` `◼` `◻` | `ICON_MD_STOP` or `ICON_MD_SQUARE` |
|
||||
| `●` `○` `◉` `◎` | `ICON_MD_FIBER_MANUAL_RECORD` or `ICON_MD_CIRCLE` |
|
||||
| `↑` `↓` `←` `→` | `ICON_MD_ARROW_UPWARD`, `_DOWNWARD`, `_BACK`, `_FORWARD` |
|
||||
| `↙` `↗` `↖` `↘` | `ICON_MD_CALL_RECEIVED`, `_MADE`, etc. |
|
||||
| `✓` `✔` | `ICON_MD_CHECK` |
|
||||
| `✗` `✕` `✖` | `ICON_MD_CLOSE` |
|
||||
| `⚠` `⚠️` | `ICON_MD_WARNING` |
|
||||
| `ℹ` `ℹ️` | `ICON_MD_INFO` |
|
||||
| `🔍` | `ICON_MD_SEARCH` |
|
||||
| `📋` | `ICON_MD_CONTENT_COPY` or `ICON_MD_DESCRIPTION` |
|
||||
| `🛡` `🛡️` | `ICON_MD_SHIELD` |
|
||||
| `⏳` | `ICON_MD_HOURGLASS_EMPTY` |
|
||||
| `🔄` `↻` `⟳` | `ICON_MD_SYNC` or `ICON_MD_REFRESH` |
|
||||
| `⚙` `⚙️` | `ICON_MD_SETTINGS` |
|
||||
| `🔒` | `ICON_MD_LOCK` |
|
||||
| `★` `☆` | `ICON_MD_STAR` or `ICON_MD_STAR_BORDER` |
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -33,4 +33,7 @@ imgui.ini
|
||||
*.bak*
|
||||
*.params
|
||||
asmap.dat
|
||||
/external/xmrig-hac
|
||||
/external/xmrig-hac
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
4
build.sh
4
build.sh
@@ -617,6 +617,10 @@ HDR
|
||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||
done
|
||||
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
|
||||
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
# ── Single-file exe (all resources embedded) ────────────────────────────
|
||||
|
||||
127
src/app.cpp
127
src/app.cpp
@@ -104,6 +104,12 @@ bool App::init()
|
||||
if (!settings_->load()) {
|
||||
DEBUG_LOGF("Warning: Could not load settings, using defaults\n");
|
||||
}
|
||||
// On upgrade (version mismatch), re-save to persist new defaults + current version
|
||||
if (settings_->needsUpgradeSave()) {
|
||||
DEBUG_LOGF("[INFO] Wallet upgraded — re-saving settings with new defaults\n");
|
||||
settings_->save();
|
||||
settings_->clearUpgradeSave();
|
||||
}
|
||||
|
||||
// Apply verbose logging preference from saved settings
|
||||
util::Logger::instance().setVerbose(settings_->getVerboseLogging());
|
||||
@@ -137,6 +143,27 @@ bool App::init()
|
||||
{
|
||||
std::string schemaPath = util::Platform::getExecutableDirectory() + "/res/themes/ui.toml";
|
||||
bool loaded = false;
|
||||
|
||||
#if HAS_EMBEDDED_UI_TOML
|
||||
// If on-disk ui.toml exists but differs in size from the embedded
|
||||
// version, a newer wallet binary is running against stale theme
|
||||
// files. Overwrite the on-disk copy so layout matches the binary.
|
||||
if (std::filesystem::exists(schemaPath)) {
|
||||
std::error_code ec;
|
||||
auto diskSize = std::filesystem::file_size(schemaPath, ec);
|
||||
if (!ec && diskSize != static_cast<std::uintmax_t>(embedded::ui_toml_size)) {
|
||||
DEBUG_LOGF("[INFO] ui.toml on disk (%ju bytes) differs from embedded (%zu bytes) — updating\n",
|
||||
(uintmax_t)diskSize, embedded::ui_toml_size);
|
||||
std::ofstream ofs(schemaPath, std::ios::binary | std::ios::trunc);
|
||||
if (ofs.is_open()) {
|
||||
ofs.write(reinterpret_cast<const char*>(embedded::ui_toml_data),
|
||||
embedded::ui_toml_size);
|
||||
ofs.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (std::filesystem::exists(schemaPath)) {
|
||||
loaded = ui::schema::UISchema::instance().loadFromFile(schemaPath);
|
||||
}
|
||||
@@ -1548,6 +1575,9 @@ void App::renderImportKeyDialog()
|
||||
return it != dlg.extraFloats.end() ? it->second : fb;
|
||||
};
|
||||
|
||||
int btnFont = (int)dlgF("button-font", 1);
|
||||
float btnW = dlgF("button-width", 120.0f);
|
||||
|
||||
if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) {
|
||||
return;
|
||||
}
|
||||
@@ -1561,6 +1591,50 @@ void App::renderImportKeyDialog()
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
|
||||
|
||||
// Paste & Clear buttons
|
||||
if (ui::material::StyledButton(TR("paste"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) {
|
||||
snprintf(import_key_input_, sizeof(import_key_input_), "%s", clipboard);
|
||||
// Trim whitespace
|
||||
std::string trimmed(import_key_input_);
|
||||
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
|
||||
trimmed.front() == '\n' || trimmed.front() == '\r'))
|
||||
trimmed.erase(trimmed.begin());
|
||||
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
|
||||
trimmed.back() == '\n' || trimmed.back() == '\r'))
|
||||
trimmed.pop_back();
|
||||
snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton(TR("clear"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
memset(import_key_input_, 0, sizeof(import_key_input_));
|
||||
}
|
||||
|
||||
// Key validation indicator
|
||||
if (import_key_input_[0] != '\0') {
|
||||
std::string k(import_key_input_);
|
||||
bool isZKey = (k.substr(0, 20) == "secret-extended-key-") ||
|
||||
(k.length() >= 2 && k[0] == 'S' && k[1] == 'K');
|
||||
bool isTKey = (k.length() >= 51 && k.length() <= 52 &&
|
||||
(k[0] == '5' || k[0] == 'K' || k[0] == 'L' || k[0] == 'U'));
|
||||
if (isZKey || isTKey) {
|
||||
ImGui::PushFont(ui::material::Type().iconSmall());
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s",
|
||||
isZKey ? "Shielded spending key" : "Transparent private key");
|
||||
} else {
|
||||
ImGui::PushFont(ui::material::Type().iconSmall());
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), ICON_MD_HELP);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Unrecognized key format");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (!import_status_.empty()) {
|
||||
@@ -1574,8 +1648,6 @@ void App::renderImportKeyDialog()
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
||||
int btnFont = (int)dlgF("button-font", 1);
|
||||
float btnW = dlgF("button-width", 120.0f);
|
||||
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
std::string key(import_key_input_);
|
||||
if (!key.empty()) {
|
||||
@@ -2037,6 +2109,57 @@ void App::rescanBlockchain()
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void App::deleteBlockchainData()
|
||||
{
|
||||
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
|
||||
ui::Notifications::instance().warning(
|
||||
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
|
||||
|
||||
std::thread([this]() {
|
||||
DEBUG_LOGF("[App] Stopping daemon for blockchain deletion...\n");
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
|
||||
for (int i = 0; i < 30 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
|
||||
// Directories to remove
|
||||
const char* dirs[] = { "blocks", "chainstate", "database", "notarizations" };
|
||||
// Files to remove
|
||||
const char* files[] = { "peers.dat", "fee_estimates.dat", "banlist.dat",
|
||||
"db.log", ".lock" };
|
||||
|
||||
int removed = 0;
|
||||
std::error_code ec;
|
||||
for (auto d : dirs) {
|
||||
fs::path p = fs::path(dataDir) / d;
|
||||
if (fs::exists(p, ec)) {
|
||||
auto n = fs::remove_all(p, ec);
|
||||
if (!ec) { removed += (int)n; DEBUG_LOGF("[App] Removed %s (%d entries)\n", d, (int)n); }
|
||||
else { DEBUG_LOGF("[App] Failed to remove %s: %s\n", d, ec.message().c_str()); }
|
||||
}
|
||||
}
|
||||
for (auto f : files) {
|
||||
fs::path p = fs::path(dataDir) / f;
|
||||
if (fs::remove(p, ec)) { removed++; DEBUG_LOGF("[App] Removed %s\n", f); }
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", removed);
|
||||
|
||||
daemon_output_offset_ = 0;
|
||||
startEmbeddedDaemon();
|
||||
}).detach();
|
||||
}
|
||||
|
||||
double App::getDaemonMemoryUsageMB() const
|
||||
{
|
||||
// If we have an embedded daemon with a tracked process handle, use it
|
||||
|
||||
@@ -253,6 +253,7 @@ public:
|
||||
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
|
||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
|
||||
void rescanBlockchain(); // restart daemon with -rescan flag
|
||||
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
||||
|
||||
// Get daemon memory usage in MB (uses embedded daemon handle if available,
|
||||
// falls back to platform-level process scan for external daemons)
|
||||
@@ -571,6 +572,7 @@ private:
|
||||
|
||||
// Mine-when-idle: auto-start/stop mining based on system idle state
|
||||
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
|
||||
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
|
||||
|
||||
// Private methods - rendering
|
||||
void renderStatusBar();
|
||||
|
||||
@@ -138,6 +138,37 @@ void App::tryConnect()
|
||||
// endlessly — tell the user what's wrong.
|
||||
bool authFailure = (connectErr.find("401") != std::string::npos);
|
||||
if (authFailure) {
|
||||
// Try .cookie auth as fallback — the daemon may have
|
||||
// generated a .cookie file instead of using DRAGONX.conf credentials
|
||||
std::string dataDir = rpc::Connection::getDefaultDataDir();
|
||||
std::string cookieUser, cookiePass;
|
||||
if (rpc::Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) {
|
||||
VERBOSE_LOGF("[connect #%d] HTTP 401 — retrying with .cookie auth from %s\n",
|
||||
attempt, dataDir.c_str());
|
||||
worker_->post([this, config, cookieUser, cookiePass, attempt]() -> rpc::RPCWorker::MainCb {
|
||||
auto cookieConfig = config;
|
||||
cookieConfig.rpcuser = cookieUser;
|
||||
cookieConfig.rpcpassword = cookiePass;
|
||||
bool ok = rpc_->connect(cookieConfig.host, cookieConfig.port, cookieConfig.rpcuser, cookieConfig.rpcpassword);
|
||||
return [this, cookieConfig, ok, attempt]() {
|
||||
connection_in_progress_ = false;
|
||||
if (ok) {
|
||||
VERBOSE_LOGF("[connect #%d] Connected via .cookie auth\n", attempt);
|
||||
saved_config_ = cookieConfig;
|
||||
onConnected();
|
||||
} else {
|
||||
state_.connected = false;
|
||||
connection_status_ = "Auth failed — check rpcuser/rpcpassword";
|
||||
VERBOSE_LOGF("[connect #%d] .cookie auth also failed\n", attempt);
|
||||
ui::Notifications::instance().error(
|
||||
"RPC authentication failed (HTTP 401). "
|
||||
"The rpcuser/rpcpassword in DRAGONX.conf don't match the running daemon. "
|
||||
"Restart the daemon or correct the credentials.");
|
||||
}
|
||||
};
|
||||
});
|
||||
return; // async retry in progress
|
||||
}
|
||||
state_.connected = false;
|
||||
std::string confPath = rpc::Connection::getDefaultConfPath();
|
||||
connection_status_ = "Auth failed — check rpcuser/rpcpassword";
|
||||
@@ -160,6 +191,17 @@ void App::tryConnect()
|
||||
connection_status_ = "Connecting to daemon...";
|
||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt);
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
} else if (connectErr.find("Loading") != std::string::npos ||
|
||||
connectErr.find("Verifying") != std::string::npos ||
|
||||
connectErr.find("Activating") != std::string::npos ||
|
||||
connectErr.find("Rewinding") != std::string::npos ||
|
||||
connectErr.find("Rescanning") != std::string::npos ||
|
||||
connectErr.find("Pruning") != std::string::npos) {
|
||||
// Daemon is reachable but still in warmup (Loading block index, etc.)
|
||||
state_.connected = false;
|
||||
connection_status_ = connectErr;
|
||||
VERBOSE_LOGF("[connect #%d] Daemon warmup: %s\n", attempt, connectErr.c_str());
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
} else {
|
||||
onDisconnected("Connection failed");
|
||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
|
||||
@@ -230,12 +272,18 @@ void App::onConnected()
|
||||
state_.protocol_version = info["protocolversion"].get<int>();
|
||||
if (info.contains("p2pport"))
|
||||
state_.p2p_port = info["p2pport"].get<int>();
|
||||
if (info.contains("longestchain"))
|
||||
state_.longestchain = info["longestchain"].get<int>();
|
||||
if (info.contains("longestchain")) {
|
||||
int lc = info["longestchain"].get<int>();
|
||||
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
|
||||
if (lc > 0) state_.longestchain = lc;
|
||||
}
|
||||
if (info.contains("notarized"))
|
||||
state_.notarized = info["notarized"].get<int>();
|
||||
if (info.contains("blocks"))
|
||||
state_.sync.blocks = info["blocks"].get<int>();
|
||||
// longestchain can lag behind blocks when peer data is stale
|
||||
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
|
||||
state_.longestchain = state_.sync.blocks;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[onConnected] getinfo callback error: %s\n", e.what());
|
||||
}
|
||||
@@ -742,8 +790,14 @@ void App::refreshData()
|
||||
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||
if (blockInfo.contains("verificationprogress"))
|
||||
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||
if (blockInfo.contains("longestchain"))
|
||||
state_.longestchain = blockInfo["longestchain"].get<int>();
|
||||
if (blockInfo.contains("longestchain")) {
|
||||
int lc = blockInfo["longestchain"].get<int>();
|
||||
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
|
||||
if (lc > 0) state_.longestchain = lc;
|
||||
}
|
||||
// longestchain can lag behind blocks when peer data is stale
|
||||
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
|
||||
state_.longestchain = state_.sync.blocks;
|
||||
// Use longestchain (actual network tip) for sync check when available,
|
||||
// since headers can be inflated by misbehaving peers.
|
||||
if (state_.longestchain > 0)
|
||||
@@ -891,8 +945,14 @@ void App::refreshBalance()
|
||||
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||
if (blockInfo.contains("verificationprogress"))
|
||||
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||
if (blockInfo.contains("longestchain"))
|
||||
state_.longestchain = blockInfo["longestchain"].get<int>();
|
||||
if (blockInfo.contains("longestchain")) {
|
||||
int lc = blockInfo["longestchain"].get<int>();
|
||||
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
|
||||
if (lc > 0) state_.longestchain = lc;
|
||||
}
|
||||
// longestchain can lag behind blocks when peer data is stale
|
||||
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
|
||||
state_.longestchain = state_.sync.blocks;
|
||||
if (state_.longestchain > 0)
|
||||
state_.sync.syncing = (state_.sync.blocks < state_.longestchain - 2);
|
||||
else
|
||||
@@ -1358,7 +1418,7 @@ void App::startPoolMining(int threads)
|
||||
|
||||
if (cfg.wallet_address.empty()) {
|
||||
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
|
||||
ui::Notifications::instance().error("No wallet address available for pool mining");
|
||||
ui::Notifications::instance().error("No wallet address available — generate a Z address in the Receive tab");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +415,9 @@ void App::checkAutoLock() {
|
||||
|
||||
// ===========================================================================
|
||||
// Mine when idle — auto-start/stop mining based on system idle state
|
||||
// Supports two modes:
|
||||
// 1. Start/Stop mode (default): start mining when idle, stop when active
|
||||
// 2. Thread scaling mode: mining stays running, thread count changes
|
||||
// ===========================================================================
|
||||
|
||||
void App::checkIdleMining() {
|
||||
@@ -422,6 +425,7 @@ void App::checkIdleMining() {
|
||||
// Feature disabled — if we previously auto-started, stop now
|
||||
if (idle_mining_active_) {
|
||||
idle_mining_active_ = false;
|
||||
idle_scaled_to_idle_ = false;
|
||||
if (settings_ && settings_->getPoolMode()) {
|
||||
if (xmrig_manager_ && xmrig_manager_->isRunning())
|
||||
stopPoolMining();
|
||||
@@ -430,46 +434,89 @@ void App::checkIdleMining() {
|
||||
stopMining();
|
||||
}
|
||||
}
|
||||
// Reset scaling state when feature is off
|
||||
if (idle_scaled_to_idle_) idle_scaled_to_idle_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int idleSec = util::Platform::getSystemIdleSeconds();
|
||||
int delay = settings_->getMineIdleDelay();
|
||||
bool isPool = settings_->getPoolMode();
|
||||
bool threadScaling = settings_->getIdleThreadScaling();
|
||||
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
||||
|
||||
// Check if mining is already running (manually started by user)
|
||||
bool miningActive = isPool
|
||||
? (xmrig_manager_ && xmrig_manager_->isRunning())
|
||||
: state_.mining.generate;
|
||||
|
||||
if (idleSec >= delay) {
|
||||
// System is idle — start mining if not already running
|
||||
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
|
||||
// For solo mining, need daemon connected and synced
|
||||
if (!isPool && (!state_.connected || state_.sync.syncing)) return;
|
||||
if (threadScaling) {
|
||||
// --- Thread scaling mode ---
|
||||
// Mining must already be running (started by user). We just adjust threads.
|
||||
if (!miningActive || mining_toggle_in_progress_.load()) return;
|
||||
|
||||
int threads = settings_->getPoolThreads();
|
||||
if (threads <= 0) threads = std::max(1, (int)std::thread::hardware_concurrency() / 2);
|
||||
int activeThreads = settings_->getIdleThreadsActive();
|
||||
int idleThreads = settings_->getIdleThreadsIdle();
|
||||
// Resolve auto values: active defaults to half, idle defaults to all
|
||||
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
|
||||
if (idleThreads <= 0) idleThreads = maxThreads;
|
||||
|
||||
idle_mining_active_ = true;
|
||||
if (isPool)
|
||||
startPoolMining(threads);
|
||||
else
|
||||
startMining(threads);
|
||||
DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec);
|
||||
if (idleSec >= delay) {
|
||||
// System is idle — scale up to idle thread count
|
||||
if (!idle_scaled_to_idle_) {
|
||||
idle_scaled_to_idle_ = true;
|
||||
if (isPool) {
|
||||
stopPoolMining();
|
||||
startPoolMining(idleThreads);
|
||||
} else {
|
||||
startMining(idleThreads);
|
||||
}
|
||||
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads);
|
||||
}
|
||||
} else {
|
||||
// User is active — scale down to active thread count
|
||||
if (idle_scaled_to_idle_) {
|
||||
idle_scaled_to_idle_ = false;
|
||||
if (isPool) {
|
||||
stopPoolMining();
|
||||
startPoolMining(activeThreads);
|
||||
} else {
|
||||
startMining(activeThreads);
|
||||
}
|
||||
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User is active — stop mining if we auto-started it
|
||||
if (idle_mining_active_) {
|
||||
idle_mining_active_ = false;
|
||||
if (isPool) {
|
||||
if (xmrig_manager_ && xmrig_manager_->isRunning())
|
||||
stopPoolMining();
|
||||
} else {
|
||||
if (state_.mining.generate)
|
||||
stopMining();
|
||||
// --- Start/Stop mode (original behavior) ---
|
||||
if (idleSec >= delay) {
|
||||
// System is idle — start mining if not already running
|
||||
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
|
||||
// For solo mining, need daemon connected and synced
|
||||
if (!isPool && (!state_.connected || state_.sync.syncing)) return;
|
||||
|
||||
int threads = settings_->getPoolThreads();
|
||||
if (threads <= 0) threads = std::max(1, maxThreads / 2);
|
||||
|
||||
idle_mining_active_ = true;
|
||||
if (isPool)
|
||||
startPoolMining(threads);
|
||||
else
|
||||
startMining(threads);
|
||||
DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec);
|
||||
}
|
||||
} else {
|
||||
// User is active — stop mining if we auto-started it
|
||||
if (idle_mining_active_) {
|
||||
idle_mining_active_ = false;
|
||||
if (isPool) {
|
||||
if (xmrig_manager_ && xmrig_manager_->isRunning())
|
||||
stopPoolMining();
|
||||
} else {
|
||||
if (state_.mining.generate)
|
||||
stopMining();
|
||||
}
|
||||
DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
|
||||
}
|
||||
DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,6 +766,15 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
if (isEmbeddedDaemonRunning()) {
|
||||
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap retry...\n");
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
try { rpc_->call("stop"); } catch (...) {}
|
||||
rpc_->disconnect();
|
||||
}
|
||||
onDisconnected("Bootstrap retry");
|
||||
}
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
bootstrap_->start(dataDir);
|
||||
@@ -894,7 +903,7 @@ void App::renderFirstRunWizard() {
|
||||
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
|
||||
const char* twText = "Only use bootstrap.dragonx.is. Using files from untrusted sources could compromise your node.";
|
||||
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
|
||||
float twWrap = contentW - iw - 4.0f * dp;
|
||||
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
|
||||
@@ -903,18 +912,29 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
// Buttons (only when focused)
|
||||
if (isFocused) {
|
||||
float dlBtnW = 180.0f * dp;
|
||||
float dlBtnW = 150.0f * dp;
|
||||
float mirrorW = 150.0f * dp;
|
||||
float skipW2 = 80.0f * dp;
|
||||
float btnH2 = 40.0f * dp;
|
||||
float totalBW = dlBtnW + 12.0f * dp + skipW2;
|
||||
float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2;
|
||||
float bx = rightX + (colW - totalBW) * 0.5f;
|
||||
|
||||
// --- Download button (main / Cloudflare) ---
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
if (isEmbeddedDaemonRunning()) {
|
||||
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n");
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
try { rpc_->call("stop"); } catch (...) {}
|
||||
rpc_->disconnect();
|
||||
}
|
||||
onDisconnected("Bootstrap");
|
||||
}
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
bootstrap_->start(dataDir);
|
||||
@@ -923,7 +943,35 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 12.0f * dp, cy));
|
||||
// --- Mirror Download button ---
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
||||
if (isEmbeddedDaemonRunning()) {
|
||||
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap (mirror)...\n");
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
try { rpc_->call("stop"); } catch (...) {}
|
||||
rpc_->disconnect();
|
||||
}
|
||||
onDisconnected("Bootstrap");
|
||||
}
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||
bootstrap_->start(dataDir, mirrorUrl);
|
||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
// --- Skip button ---
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
|
||||
@@ -150,6 +150,9 @@ bool Settings::load(const std::string& path)
|
||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
||||
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
|
||||
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
|
||||
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
|
||||
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
|
||||
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
|
||||
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||
saved_pool_urls_.clear();
|
||||
for (const auto& u : j["saved_pool_urls"])
|
||||
@@ -166,7 +169,16 @@ bool Settings::load(const std::string& path)
|
||||
window_width_ = j["window_width"].get<int>();
|
||||
if (j.contains("window_height") && j["window_height"].is_number_integer())
|
||||
window_height_ = j["window_height"].get<int>();
|
||||
|
||||
|
||||
// Version tracking — detect upgrades so we can re-save with new defaults
|
||||
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
|
||||
if (settings_version_ != DRAGONX_VERSION) {
|
||||
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
|
||||
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
|
||||
DRAGONX_VERSION);
|
||||
needs_upgrade_save_ = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
||||
@@ -235,6 +247,9 @@ bool Settings::save(const std::string& path)
|
||||
j["pool_mode"] = pool_mode_;
|
||||
j["mine_when_idle"] = mine_when_idle_;
|
||||
j["mine_idle_delay"]= mine_idle_delay_;
|
||||
j["idle_thread_scaling"] = idle_thread_scaling_;
|
||||
j["idle_threads_active"] = idle_threads_active_;
|
||||
j["idle_threads_idle"] = idle_threads_idle_;
|
||||
j["saved_pool_urls"] = json::array();
|
||||
for (const auto& u : saved_pool_urls_)
|
||||
j["saved_pool_urls"].push_back(u);
|
||||
@@ -242,6 +257,7 @@ bool Settings::save(const std::string& path)
|
||||
for (const auto& w : saved_pool_workers_)
|
||||
j["saved_pool_workers"].push_back(w);
|
||||
j["font_scale"] = font_scale_;
|
||||
j["settings_version"] = std::string(DRAGONX_VERSION);
|
||||
if (window_width_ > 0 && window_height_ > 0) {
|
||||
j["window_width"] = window_width_;
|
||||
j["window_height"] = window_height_;
|
||||
|
||||
@@ -214,6 +214,14 @@ public:
|
||||
int getMineIdleDelay() const { return mine_idle_delay_; }
|
||||
void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); }
|
||||
|
||||
// Idle thread scaling — scale thread count instead of start/stop
|
||||
bool getIdleThreadScaling() const { return idle_thread_scaling_; }
|
||||
void setIdleThreadScaling(bool v) { idle_thread_scaling_ = v; }
|
||||
int getIdleThreadsActive() const { return idle_threads_active_; }
|
||||
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
|
||||
int getIdleThreadsIdle() const { return idle_threads_idle_; }
|
||||
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
|
||||
|
||||
// Saved pool URLs (user-managed favorites dropdown)
|
||||
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
|
||||
void addSavedPoolUrl(const std::string& url) {
|
||||
@@ -248,6 +256,10 @@ public:
|
||||
int getWindowHeight() const { return window_height_; }
|
||||
void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; }
|
||||
|
||||
// Returns true once after an upgrade (version mismatch detected on load)
|
||||
bool needsUpgradeSave() const { return needs_upgrade_save_; }
|
||||
void clearUpgradeSave() { needs_upgrade_save_ = false; }
|
||||
|
||||
private:
|
||||
std::string settings_path_;
|
||||
|
||||
@@ -290,13 +302,16 @@ private:
|
||||
// Pool mining
|
||||
std::string pool_url_ = "pool.dragonx.is:3433";
|
||||
std::string pool_algo_ = "rx/hush";
|
||||
std::string pool_worker_ = "x";
|
||||
std::string pool_worker_ = "";
|
||||
int pool_threads_ = 0;
|
||||
bool pool_tls_ = false;
|
||||
bool pool_hugepages_ = true;
|
||||
bool pool_mode_ = false; // false=solo, true=pool
|
||||
bool mine_when_idle_ = false; // auto-start mining when system idle
|
||||
int mine_idle_delay_= 120; // seconds of idle before mining starts
|
||||
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
|
||||
int idle_threads_active_ = 0; // threads when user active (0 = auto)
|
||||
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
|
||||
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
|
||||
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
||||
|
||||
@@ -306,6 +321,10 @@ private:
|
||||
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
|
||||
int window_width_ = 0;
|
||||
int window_height_ = 0;
|
||||
|
||||
// Wallet version that last wrote this settings file (for upgrade detection)
|
||||
std::string settings_version_;
|
||||
bool needs_upgrade_save_ = false; // true when version changed
|
||||
};
|
||||
|
||||
} // namespace config
|
||||
|
||||
11
src/main.cpp
11
src/main.cpp
@@ -1800,6 +1800,17 @@ int main(int argc, char* argv[])
|
||||
// while background cleanup (thread joins, RPC disconnect) continues.
|
||||
SDL_HideWindow(window);
|
||||
|
||||
// Watchdog: if cleanup takes too long the process lingers without a
|
||||
// window, showing up as a "Background Service" in Task Manager.
|
||||
// Force-exit after 3 seconds — all critical state (settings, daemon
|
||||
// stop) was handled in beginShutdown().
|
||||
std::thread([]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_Exit(0);
|
||||
}).detach();
|
||||
|
||||
// Final cleanup (daemon already stopped by beginShutdown)
|
||||
app.shutdown();
|
||||
#ifdef DRAGONX_USE_DX11
|
||||
|
||||
@@ -144,6 +144,37 @@ int extractBundledThemes(const std::string& destDir)
|
||||
return count;
|
||||
}
|
||||
|
||||
int updateBundledThemes(const std::string& dir)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
int count = 0;
|
||||
|
||||
const auto* themes = getEmbeddedThemes();
|
||||
if (!themes || !themes->data) return 0;
|
||||
if (!fs::exists(dir)) return 0;
|
||||
|
||||
for (const auto* t = themes; t->data != nullptr; ++t) {
|
||||
fs::path dest = fs::path(dir) / t->filename;
|
||||
if (!fs::exists(dest)) {
|
||||
// New theme not yet on disk — extract it
|
||||
} else {
|
||||
std::error_code ec;
|
||||
auto diskSize = fs::file_size(dest, ec);
|
||||
if (!ec && diskSize == static_cast<std::uintmax_t>(t->size))
|
||||
continue; // up to date
|
||||
}
|
||||
std::ofstream f(dest, std::ios::binary);
|
||||
if (f.is_open()) {
|
||||
f.write(reinterpret_cast<const char*>(t->data), t->size);
|
||||
f.close();
|
||||
DEBUG_LOGF("[INFO] EmbeddedResources: Updated stale theme: %s (%zu bytes)\n",
|
||||
t->filename, t->size);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::string getParamsDirectory()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
@@ -169,6 +200,9 @@ std::string getParamsDirectory()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Forward declaration — defined below extractResource()
|
||||
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath);
|
||||
|
||||
bool needsParamsExtraction()
|
||||
{
|
||||
if (!hasEmbeddedResources()) {
|
||||
@@ -177,21 +211,46 @@ bool needsParamsExtraction()
|
||||
|
||||
// Check daemon directory (dragonx/) — the only extraction target
|
||||
std::string daemonDir = getDaemonDirectory();
|
||||
std::string spendPath = daemonDir +
|
||||
#ifdef _WIN32
|
||||
"\\sapling-spend.params";
|
||||
const char pathSep = '\\';
|
||||
#else
|
||||
"/sapling-spend.params";
|
||||
#endif
|
||||
std::string outputPath = daemonDir +
|
||||
#ifdef _WIN32
|
||||
"\\sapling-output.params";
|
||||
#else
|
||||
"/sapling-output.params";
|
||||
const char pathSep = '/';
|
||||
#endif
|
||||
std::string spendPath = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
|
||||
std::string outputPath = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
|
||||
|
||||
// Check if both params exist in daemon directory
|
||||
return !std::filesystem::exists(spendPath) || !std::filesystem::exists(outputPath);
|
||||
// Check if params are missing or stale (size mismatch → updated in newer build)
|
||||
const auto* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
|
||||
const auto* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
|
||||
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
|
||||
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
|
||||
|
||||
// Also check if daemon binaries need updating
|
||||
#ifdef HAS_EMBEDDED_DAEMON
|
||||
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||
if (daemonRes && resourceNeedsUpdate(daemonRes, daemonPath)) return true;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EMBEDDED_XMRIG
|
||||
const auto* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
|
||||
std::string xmrigPath = daemonDir + pathSep + RESOURCE_XMRIG;
|
||||
if (xmrigRes && resourceNeedsUpdate(xmrigRes, xmrigPath)) return true;
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if an on-disk file is missing or differs in size from the embedded resource.
|
||||
// A size mismatch means the binary was updated in a newer wallet build.
|
||||
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath)
|
||||
{
|
||||
if (!res || !res->data || res->size == 0) return false;
|
||||
if (!std::filesystem::exists(destPath)) return true;
|
||||
std::error_code ec;
|
||||
auto diskSize = std::filesystem::file_size(destPath, ec);
|
||||
if (ec) return true; // can't stat → re-extract
|
||||
return diskSize != static_cast<std::uintmax_t>(res->size);
|
||||
}
|
||||
|
||||
static bool extractResource(const EmbeddedResource* res, const std::string& destPath)
|
||||
@@ -251,7 +310,7 @@ bool extractEmbeddedResources()
|
||||
const EmbeddedResource* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
|
||||
if (spendRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
if (resourceNeedsUpdate(spendRes, dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting sapling-spend.params (%zu MB)...\n", spendRes->size / (1024*1024));
|
||||
if (!extractResource(spendRes, dest)) {
|
||||
success = false;
|
||||
@@ -262,7 +321,7 @@ bool extractEmbeddedResources()
|
||||
const EmbeddedResource* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
|
||||
if (outputRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
if (resourceNeedsUpdate(outputRes, dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting sapling-output.params (%zu MB)...\n", outputRes->size / (1024*1024));
|
||||
if (!extractResource(outputRes, dest)) {
|
||||
success = false;
|
||||
@@ -274,7 +333,7 @@ bool extractEmbeddedResources()
|
||||
const EmbeddedResource* asmapRes = getEmbeddedResource(RESOURCE_ASMAP);
|
||||
if (asmapRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_ASMAP;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
if (resourceNeedsUpdate(asmapRes, dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting asmap.dat...\n");
|
||||
if (!extractResource(asmapRes, dest)) {
|
||||
success = false;
|
||||
@@ -291,33 +350,48 @@ bool extractEmbeddedResources()
|
||||
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||
if (daemonRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonxd.exe (%zu MB)...\n", daemonRes->size / (1024*1024));
|
||||
if (resourceNeedsUpdate(daemonRes, dest)) {
|
||||
if (std::filesystem::exists(dest))
|
||||
DEBUG_LOGF("[INFO] Updating stale dragonxd (size mismatch)...\n");
|
||||
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
|
||||
if (!extractResource(daemonRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
|
||||
if (cliRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-cli.exe (%zu MB)...\n", cliRes->size / (1024*1024));
|
||||
if (resourceNeedsUpdate(cliRes, dest)) {
|
||||
if (std::filesystem::exists(dest))
|
||||
DEBUG_LOGF("[INFO] Updating stale dragonx-cli (size mismatch)...\n");
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
|
||||
if (!extractResource(cliRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
|
||||
if (txRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-tx.exe (%zu MB)...\n", txRes->size / (1024*1024));
|
||||
if (resourceNeedsUpdate(txRes, dest)) {
|
||||
if (std::filesystem::exists(dest))
|
||||
DEBUG_LOGF("[INFO] Updating stale dragonx-tx (size mismatch)...\n");
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
|
||||
if (!extractResource(txRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -326,11 +400,16 @@ bool extractEmbeddedResources()
|
||||
const EmbeddedResource* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
|
||||
if (xmrigRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_XMRIG;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting xmrig.exe (%zu MB)...\n", xmrigRes->size / (1024*1024));
|
||||
if (resourceNeedsUpdate(xmrigRes, dest)) {
|
||||
if (std::filesystem::exists(dest))
|
||||
DEBUG_LOGF("[INFO] Updating stale xmrig (size mismatch)...\n");
|
||||
DEBUG_LOGF("[INFO] Extracting xmrig (%zu MB)...\n", xmrigRes->size / (1024*1024));
|
||||
if (!extractResource(xmrigRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -360,7 +439,8 @@ bool needsDaemonExtraction()
|
||||
#else
|
||||
std::string daemonPath = daemonDir + "/dragonxd";
|
||||
#endif
|
||||
return !std::filesystem::exists(daemonPath);
|
||||
const auto* res = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||
return resourceNeedsUpdate(res, daemonPath);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
@@ -426,7 +506,8 @@ bool needsXmrigExtraction()
|
||||
#else
|
||||
std::string xmrigPath = daemonDir + "/xmrig";
|
||||
#endif
|
||||
return !std::filesystem::exists(xmrigPath);
|
||||
const auto* res = getEmbeddedResource(RESOURCE_XMRIG);
|
||||
return resourceNeedsUpdate(res, xmrigPath);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
||||
@@ -55,6 +55,11 @@ const EmbeddedTheme* getEmbeddedThemes();
|
||||
// Returns number of themes extracted
|
||||
int extractBundledThemes(const std::string& destDir);
|
||||
|
||||
// Update stale bundled theme files in the given directory.
|
||||
// Compares on-disk file size to embedded size; overwrites on mismatch.
|
||||
// Returns number of themes updated.
|
||||
int updateBundledThemes(const std::string& dir);
|
||||
|
||||
// Check if daemon needs to be extracted
|
||||
bool needsDaemonExtraction();
|
||||
|
||||
|
||||
@@ -118,13 +118,16 @@ ConnectionConfig Connection::parseConfFile(const std::string& path)
|
||||
std::string key = line.substr(0, eq_pos);
|
||||
std::string value = line.substr(eq_pos + 1);
|
||||
|
||||
// Trim whitespace
|
||||
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) {
|
||||
// Trim whitespace (including \r from Windows line endings)
|
||||
while (!key.empty() && (key.back() == ' ' || key.back() == '\t' || key.back() == '\r')) {
|
||||
key.pop_back();
|
||||
}
|
||||
while (!value.empty() && (value[0] == ' ' || value[0] == '\t')) {
|
||||
value.erase(0, 1);
|
||||
}
|
||||
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' || value.back() == '\r')) {
|
||||
value.pop_back();
|
||||
}
|
||||
|
||||
// Map to config
|
||||
if (key == "rpcuser") {
|
||||
@@ -172,6 +175,16 @@ ConnectionConfig Connection::autoDetectConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// If rpcpassword is empty, the daemon may be using .cookie auth
|
||||
if (config.rpcpassword.empty()) {
|
||||
std::string cookieUser, cookiePass;
|
||||
if (readAuthCookie(data_dir, cookieUser, cookiePass)) {
|
||||
config.rpcuser = cookieUser;
|
||||
config.rpcpassword = cookiePass;
|
||||
DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaults for missing values
|
||||
if (config.host.empty()) {
|
||||
config.host = DRAGONX_DEFAULT_RPC_HOST;
|
||||
@@ -319,5 +332,40 @@ bool Connection::ensureEncryptionEnabled(const std::string& confPath)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Connection::readAuthCookie(const std::string& dataDir, std::string& user, std::string& password)
|
||||
{
|
||||
if (dataDir.empty()) return false;
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string cookiePath = dataDir + "\\.cookie";
|
||||
#else
|
||||
std::string cookiePath = dataDir + "/.cookie";
|
||||
#endif
|
||||
|
||||
std::ifstream file(cookiePath);
|
||||
if (!file.is_open()) return false;
|
||||
|
||||
std::string cookie;
|
||||
std::getline(file, cookie);
|
||||
file.close();
|
||||
|
||||
// Cookie format: __cookie__:base64encodedpassword
|
||||
size_t colonPos = cookie.find(':');
|
||||
if (colonPos == std::string::npos || colonPos == 0) return false;
|
||||
|
||||
user = cookie.substr(0, colonPos);
|
||||
password = cookie.substr(colonPos + 1);
|
||||
|
||||
// Trim \r if present (Windows line endings)
|
||||
while (!password.empty() && (password.back() == '\r' || password.back() == '\n')) {
|
||||
password.pop_back();
|
||||
}
|
||||
|
||||
if (user.empty() || password.empty()) return false;
|
||||
|
||||
DEBUG_LOGF("Read auth cookie from: %s (user=%s)\n", cookiePath.c_str(), user.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -87,6 +87,15 @@ public:
|
||||
*/
|
||||
static bool ensureEncryptionEnabled(const std::string& confPath);
|
||||
|
||||
/**
|
||||
* @brief Try to read .cookie auth file from the data directory
|
||||
* @param dataDir Path to the daemon data directory
|
||||
* @param user Output: cookie username (__cookie__)
|
||||
* @param password Output: cookie password
|
||||
* @return true if cookie file was read successfully
|
||||
*/
|
||||
static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password);
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
|
||||
@@ -100,7 +100,20 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
last_connect_error_ = e.what();
|
||||
DEBUG_LOGF("Connection failed: %s\n", e.what());
|
||||
// Daemon warmup messages (Loading block index, Verifying blocks, etc.)
|
||||
// are normal startup progress — don't label them "Connection failed".
|
||||
std::string msg = e.what();
|
||||
bool isWarmup = (msg.find("Loading") != std::string::npos ||
|
||||
msg.find("Verifying") != std::string::npos ||
|
||||
msg.find("Activating") != std::string::npos ||
|
||||
msg.find("Rewinding") != std::string::npos ||
|
||||
msg.find("Rescanning") != std::string::npos ||
|
||||
msg.find("Pruning") != std::string::npos);
|
||||
if (isWarmup) {
|
||||
DEBUG_LOGF("Daemon starting: %s\n", msg.c_str());
|
||||
} else {
|
||||
DEBUG_LOGF("Connection failed: %s\n", msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
connected_ = false;
|
||||
|
||||
@@ -116,6 +116,9 @@ static bool sp_stop_external_daemon = false;
|
||||
// Mining — mine when idle
|
||||
static bool sp_mine_when_idle = false;
|
||||
static int sp_mine_idle_delay = 120;
|
||||
static bool sp_idle_thread_scaling = false;
|
||||
static int sp_idle_threads_active = 0;
|
||||
static int sp_idle_threads_idle = 0;
|
||||
static bool sp_verbose_logging = false;
|
||||
|
||||
// Debug logging categories
|
||||
@@ -125,6 +128,7 @@ static bool sp_debug_expanded = false; // collapsible card state
|
||||
static bool sp_effects_expanded = false; // "Advanced Effects..." toggle
|
||||
static bool sp_tools_expanded = false; // "Tools & Actions..." toggle
|
||||
static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history
|
||||
static bool sp_confirm_delete_blockchain = false; // confirmation dialog for deleting blockchain data
|
||||
|
||||
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
||||
|
||||
@@ -181,6 +185,9 @@ static void loadSettingsPageState(config::Settings* settings) {
|
||||
sp_stop_external_daemon = settings->getStopExternalDaemon();
|
||||
sp_mine_when_idle = settings->getMineWhenIdle();
|
||||
sp_mine_idle_delay = settings->getMineIdleDelay();
|
||||
sp_idle_thread_scaling = settings->getIdleThreadScaling();
|
||||
sp_idle_threads_active = settings->getIdleThreadsActive();
|
||||
sp_idle_threads_idle = settings->getIdleThreadsIdle();
|
||||
sp_verbose_logging = settings->getVerboseLogging();
|
||||
sp_debug_categories = settings->getDebugCategories();
|
||||
sp_debug_cats_dirty = false;
|
||||
@@ -230,6 +237,9 @@ static void saveSettingsPageState(config::Settings* settings) {
|
||||
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
||||
settings->setMineWhenIdle(sp_mine_when_idle);
|
||||
settings->setMineIdleDelay(sp_mine_idle_delay);
|
||||
settings->setIdleThreadScaling(sp_idle_thread_scaling);
|
||||
settings->setIdleThreadsActive(sp_idle_threads_active);
|
||||
settings->setIdleThreadsIdle(sp_idle_threads_idle);
|
||||
settings->setVerboseLogging(sp_verbose_logging);
|
||||
settings->setDebugCategories(sp_debug_categories);
|
||||
|
||||
@@ -1485,6 +1495,15 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
// Delete blockchain button (always available when using embedded daemon)
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm()));
|
||||
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
|
||||
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) {
|
||||
sp_confirm_delete_blockchain = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::PopFont();
|
||||
@@ -1737,6 +1756,20 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
|
||||
ImGui::PopFont();
|
||||
|
||||
// Daemon version
|
||||
{
|
||||
const auto& st = app->state();
|
||||
if (st.daemon_version > 0) {
|
||||
int dmaj = st.daemon_version / 1000000;
|
||||
int dmin = (st.daemon_version / 10000) % 100;
|
||||
int dpat = (st.daemon_version / 100) % 100;
|
||||
ImGui::PushFont(body2);
|
||||
snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat);
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf);
|
||||
ImGui::PopFont();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
ImGui::PushFont(body2);
|
||||
@@ -2009,6 +2042,39 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation dialog for deleting blockchain data
|
||||
if (sp_confirm_delete_blockchain) {
|
||||
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &sp_confirm_delete_blockchain, 500.0f, 0.94f)) {
|
||||
ImGui::PushFont(Type().iconLarge());
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
sp_confirm_delete_blockchain = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
|
||||
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
|
||||
app->deleteBlockchainData();
|
||||
sp_confirm_delete_blockchain = false;
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -1,848 +0,0 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "settings_page.h"
|
||||
#include "../../app.h"
|
||||
#include "../../version.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../rpc/rpc_client.h"
|
||||
#include "../theme.h"
|
||||
#include "../layout.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../schema/skin_manager.h"
|
||||
#include "../notifications.h"
|
||||
#include "../effects/imgui_acrylic.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../windows/validate_address_dialog.h"
|
||||
#include "../windows/address_book_dialog.h"
|
||||
#include "../windows/shield_dialog.h"
|
||||
#include "../windows/request_payment_dialog.h"
|
||||
#include "../windows/block_info_dialog.h"
|
||||
#include "../windows/export_all_keys_dialog.h"
|
||||
#include "../windows/export_transactions_dialog.h"
|
||||
#include "imgui.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
using namespace material;
|
||||
|
||||
// ============================================================================
|
||||
// Settings state — loaded from config::Settings on first render
|
||||
// ============================================================================
|
||||
static bool sp_initialized = false;
|
||||
static int sp_language_index = 0;
|
||||
static bool sp_save_ztxs = true;
|
||||
static bool sp_allow_custom_fees = false;
|
||||
static bool sp_auto_shield = false;
|
||||
static bool sp_fetch_prices = true;
|
||||
static bool sp_use_tor = false;
|
||||
static char sp_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
|
||||
static char sp_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
|
||||
static char sp_rpc_user[64] = "";
|
||||
static char sp_rpc_password[64] = "";
|
||||
static char sp_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
|
||||
static char sp_addr_explorer[256] = "https://explorer.dragonx.is/address/";
|
||||
|
||||
// Acrylic settings
|
||||
static bool sp_acrylic_enabled = true;
|
||||
static int sp_acrylic_quality = 2;
|
||||
static float sp_blur_multiplier = 1.0f;
|
||||
static bool sp_reduced_transparency = false;
|
||||
|
||||
static void loadSettingsPageState(config::Settings* settings) {
|
||||
if (!settings) return;
|
||||
|
||||
sp_save_ztxs = settings->getSaveZtxs();
|
||||
sp_allow_custom_fees = settings->getAllowCustomFees();
|
||||
sp_auto_shield = settings->getAutoShield();
|
||||
sp_fetch_prices = settings->getFetchPrices();
|
||||
sp_use_tor = settings->getUseTor();
|
||||
|
||||
strncpy(sp_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(sp_tx_explorer) - 1);
|
||||
strncpy(sp_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(sp_addr_explorer) - 1);
|
||||
|
||||
auto& i18n = util::I18n::instance();
|
||||
const auto& languages = i18n.getAvailableLanguages();
|
||||
std::string current_lang = settings->getLanguage();
|
||||
if (current_lang.empty()) current_lang = "en";
|
||||
|
||||
sp_language_index = 0;
|
||||
int idx = 0;
|
||||
for (const auto& lang : languages) {
|
||||
if (lang.first == current_lang) {
|
||||
sp_language_index = idx;
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
sp_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
|
||||
sp_acrylic_quality = static_cast<int>(effects::ImGuiAcrylic::GetQuality());
|
||||
sp_blur_multiplier = effects::ImGuiAcrylic::GetBlurMultiplier();
|
||||
sp_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
|
||||
|
||||
sp_initialized = true;
|
||||
}
|
||||
|
||||
static void saveSettingsPageState(config::Settings* settings) {
|
||||
if (!settings) return;
|
||||
|
||||
settings->setTheme(settings->getSkinId());
|
||||
settings->setSaveZtxs(sp_save_ztxs);
|
||||
settings->setAllowCustomFees(sp_allow_custom_fees);
|
||||
settings->setAutoShield(sp_auto_shield);
|
||||
settings->setFetchPrices(sp_fetch_prices);
|
||||
settings->setUseTor(sp_use_tor);
|
||||
settings->setTxExplorerUrl(sp_tx_explorer);
|
||||
settings->setAddressExplorerUrl(sp_addr_explorer);
|
||||
|
||||
auto& i18n = util::I18n::instance();
|
||||
const auto& languages = i18n.getAvailableLanguages();
|
||||
auto it = languages.begin();
|
||||
std::advance(it, sp_language_index);
|
||||
if (it != languages.end()) {
|
||||
settings->setLanguage(it->first);
|
||||
}
|
||||
|
||||
settings->save();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Settings Page Renderer
|
||||
// ============================================================================
|
||||
|
||||
void RenderSettingsPage(App* app) {
|
||||
// Load settings state on first render
|
||||
if (!sp_initialized && app->settings()) {
|
||||
loadSettingsPageState(app->settings());
|
||||
}
|
||||
|
||||
auto& S = schema::UI();
|
||||
|
||||
// Responsive layout — matches other tabs
|
||||
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
|
||||
float availWidth = contentAvail.x;
|
||||
float hs = Layout::hScale(availWidth);
|
||||
float vs = Layout::vScale(contentAvail.y);
|
||||
float pad = Layout::cardInnerPadding();
|
||||
float gap = Layout::cardGap();
|
||||
float glassRound = Layout::glassRounding();
|
||||
(void)vs;
|
||||
|
||||
char buf[256];
|
||||
|
||||
// Label column position — adaptive to width
|
||||
float labelW = std::max(100.0f, 120.0f * hs);
|
||||
// Input field width — fill remaining space in card
|
||||
float inputW = std::max(180.0f, availWidth - labelW - pad * 3);
|
||||
|
||||
// Scrollable content area — NoBackground matches other tabs
|
||||
ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 0), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
// Get draw list AFTER BeginChild so we draw on the child window's list
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
GlassPanelSpec glassSpec;
|
||||
glassSpec.rounding = glassRound;
|
||||
ImFont* ovFont = Type().overline();
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* body2 = Type().body2();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
|
||||
// ====================================================================
|
||||
// GENERAL — Appearance & Preferences card
|
||||
// ====================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "APPEARANCE");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Measure content height for card
|
||||
// We'll use ImGui cursor-based layout inside the card
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
|
||||
// Use a child window inside the glass panel for layout
|
||||
// First draw the glass panel, then place content
|
||||
// We need to estimate height — use a generous estimate and clip
|
||||
float rowH = body2->LegacySize + Layout::spacingSm();
|
||||
float sectionGap = Layout::spacingMd();
|
||||
float cardH = pad // top pad
|
||||
+ rowH // Theme
|
||||
+ rowH // Language
|
||||
+ sectionGap
|
||||
+ rowH * 5 // Visual effects (acrylic + quality + blur + reduce + gap)
|
||||
+ pad; // bottom pad
|
||||
if (!sp_acrylic_enabled) cardH -= rowH * 2;
|
||||
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
// --- Theme row ---
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Theme");
|
||||
ImGui::SameLine(labelW);
|
||||
|
||||
auto& skinMgr = schema::SkinManager::instance();
|
||||
const auto& skins = skinMgr.available();
|
||||
|
||||
std::string active_preview = "DragonX";
|
||||
bool active_is_custom = false;
|
||||
for (const auto& skin : skins) {
|
||||
if (skin.id == skinMgr.activeSkinId()) {
|
||||
active_preview = skin.name;
|
||||
active_is_custom = !skin.bundled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float refreshBtnW = 80.0f;
|
||||
ImGui::SetNextItemWidth(inputW - refreshBtnW - Layout::spacingSm());
|
||||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||||
ImGui::TextDisabled("Built-in");
|
||||
ImGui::Separator();
|
||||
for (size_t i = 0; i < skins.size(); i++) {
|
||||
const auto& skin = skins[i];
|
||||
if (!skin.bundled) continue;
|
||||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
||||
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
|
||||
skinMgr.setActiveSkin(skin.id);
|
||||
if (app->settings()) {
|
||||
app->settings()->setSkinId(skin.id);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
if (is_selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
bool has_custom = false;
|
||||
for (const auto& skin : skins) {
|
||||
if (!skin.bundled) { has_custom = true; break; }
|
||||
}
|
||||
if (has_custom) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Custom");
|
||||
ImGui::Separator();
|
||||
for (size_t i = 0; i < skins.size(); i++) {
|
||||
const auto& skin = skins[i];
|
||||
if (skin.bundled) continue;
|
||||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
||||
if (!skin.valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::BeginDisabled(true);
|
||||
std::string lbl = skin.name + " (invalid)";
|
||||
ImGui::Selectable(lbl.c_str(), false);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip("%s", skin.validationError.c_str());
|
||||
} else {
|
||||
std::string lbl = skin.name;
|
||||
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
|
||||
if (ImGui::Selectable(lbl.c_str(), is_selected)) {
|
||||
skinMgr.setActiveSkin(skin.id);
|
||||
if (app->settings()) {
|
||||
app->settings()->setSkinId(skin.id);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
if (is_selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (active_is_custom) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Custom theme active");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton("Refresh", ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
||||
schema::SkinManager::instance().refresh();
|
||||
Notifications::instance().info("Theme list refreshed");
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
|
||||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||||
}
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// --- Language row ---
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Language");
|
||||
ImGui::SameLine(labelW);
|
||||
|
||||
auto& i18n = util::I18n::instance();
|
||||
const auto& languages = i18n.getAvailableLanguages();
|
||||
std::vector<const char*> lang_names;
|
||||
lang_names.reserve(languages.size());
|
||||
for (const auto& lang : languages) {
|
||||
lang_names.push_back(lang.second.c_str());
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(inputW);
|
||||
if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(),
|
||||
static_cast<int>(lang_names.size()))) {
|
||||
auto it = languages.begin();
|
||||
std::advance(it, sp_language_index);
|
||||
i18n.loadLanguage(it->first);
|
||||
}
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// --- Visual Effects subsection ---
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImGui::GetCursorScreenPos(), OnSurfaceMedium(), "VISUAL EFFECTS");
|
||||
ImGui::Dummy(ImVec2(0, ovFont->LegacySize + Layout::spacingXs()));
|
||||
|
||||
{
|
||||
// Two-column: left = acrylic toggle + reduce toggle, right = quality + blur
|
||||
float colW = (availWidth - pad * 2 - Layout::spacingLg()) * 0.5f;
|
||||
|
||||
if (ImGui::Checkbox("Acrylic effects", &sp_acrylic_enabled)) {
|
||||
effects::ImGuiAcrylic::SetEnabled(sp_acrylic_enabled);
|
||||
}
|
||||
|
||||
if (sp_acrylic_enabled) {
|
||||
ImGui::SameLine(labelW + colW + Layout::spacingLg());
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Quality");
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
const char* quality_levels[] = { "Off", "Low", "Medium", "High" };
|
||||
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
|
||||
if (ImGui::Combo("##AcrylicQuality", &sp_acrylic_quality, quality_levels,
|
||||
IM_ARRAYSIZE(quality_levels))) {
|
||||
effects::ImGuiAcrylic::SetQuality(
|
||||
static_cast<effects::AcrylicQuality>(sp_acrylic_quality));
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Reduce transparency", &sp_reduced_transparency)) {
|
||||
effects::ImGuiAcrylic::SetReducedTransparency(sp_reduced_transparency);
|
||||
}
|
||||
|
||||
if (sp_acrylic_enabled) {
|
||||
ImGui::SameLine(labelW + colW + Layout::spacingLg());
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Blur");
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
|
||||
if (ImGui::SliderFloat("##BlurAmount", &sp_blur_multiplier, 0.5f, 2.0f, "%.1fx")) {
|
||||
effects::ImGuiAcrylic::SetBlurMultiplier(sp_blur_multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate actual card bottom from cursor
|
||||
ImVec2 cardEnd = ImGui::GetCursorScreenPos();
|
||||
float actualH = (cardEnd.y - cardMin.y) + pad;
|
||||
if (actualH != cardH) {
|
||||
// Redraw glass panel with correct height
|
||||
cardMax.y = cardMin.y + actualH;
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// PRIVACY & OPTIONS — Two cards side by side
|
||||
// ====================================================================
|
||||
{
|
||||
float colW = (availWidth - gap) * 0.5f;
|
||||
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
|
||||
|
||||
// --- Privacy card (left) ---
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PRIVACY");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
|
||||
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
ImGui::Checkbox("Save shielded tx history", &sp_save_ztxs);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::Checkbox("Auto-shield transparent funds", &sp_auto_shield);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::Checkbox("Use Tor for connections", &sp_use_tor);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(colW, 0));
|
||||
}
|
||||
|
||||
// --- Options card (right) ---
|
||||
{
|
||||
float rightX = rowOrigin.x + colW + gap;
|
||||
// Position cursor at the same Y as privacy label
|
||||
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "OPTIONS");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
|
||||
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
ImGui::Checkbox("Allow custom transaction fees", &sp_allow_custom_fees);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::Checkbox("Fetch price data from CoinGecko", &sp_fetch_prices);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(colW, 0));
|
||||
}
|
||||
|
||||
// Advance past the side-by-side row
|
||||
// Find the maximum bottom
|
||||
float rowBottom = ImGui::GetCursorScreenPos().y;
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// EXPLORER URLS + SAVE — card
|
||||
// ====================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK EXPLORER & SETTINGS");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float rowH = body2->LegacySize + Layout::spacingSm();
|
||||
float cardH = pad + rowH * 2 + Layout::spacingSm()
|
||||
+ body2->LegacySize + Layout::spacingMd() // save/reset row
|
||||
+ pad;
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
// Transaction URL
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Transaction URL");
|
||||
ImGui::SameLine(labelW);
|
||||
ImGui::SetNextItemWidth(inputW);
|
||||
ImGui::InputText("##TxExplorer", sp_tx_explorer, sizeof(sp_tx_explorer));
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
// Address URL
|
||||
{
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Address URL");
|
||||
ImGui::SameLine(labelW);
|
||||
ImGui::SetNextItemWidth(inputW);
|
||||
ImGui::InputText("##AddrExplorer", sp_addr_explorer, sizeof(sp_addr_explorer));
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Save / Reset — right-aligned
|
||||
{
|
||||
float saveBtnW = 120.0f;
|
||||
float resetBtnW = 140.0f;
|
||||
float btnGap = Layout::spacingSm();
|
||||
|
||||
if (TactileButton("Save Settings", ImVec2(saveBtnW, 0), S.resolveFont("button"))) {
|
||||
saveSettingsPageState(app->settings());
|
||||
Notifications::instance().success("Settings saved");
|
||||
}
|
||||
ImGui::SameLine(0, btnGap);
|
||||
if (TactileButton("Reset to Defaults", ImVec2(resetBtnW, 0), S.resolveFont("button"))) {
|
||||
if (app->settings()) {
|
||||
loadSettingsPageState(app->settings());
|
||||
Notifications::instance().info("Settings reloaded from disk");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// KEYS & BACKUP — card with two rows
|
||||
// ====================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "KEYS & BACKUP");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
||||
float cardH = pad + btnRowH * 2 + Layout::spacingSm() + pad;
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
// Keys row — spread buttons across width
|
||||
{
|
||||
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
|
||||
if (TactileButton("Import Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
app->showImportKeyDialog();
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Export Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
app->showExportKeyDialog();
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Export All Keys...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
ExportAllKeysDialog::show();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Backup row
|
||||
{
|
||||
float btnW = (availWidth - pad * 2 - Layout::spacingSm()) / 2.0f;
|
||||
if (TactileButton("Backup wallet.dat...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
app->showBackupDialog();
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Export Transactions CSV...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
ExportTransactionsDialog::show();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// WALLET — Two cards side by side: Tools | Maintenance
|
||||
// ====================================================================
|
||||
{
|
||||
float colW = (availWidth - gap) * 0.5f;
|
||||
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
|
||||
float btnH = std::max(28.0f, 34.0f * vs);
|
||||
|
||||
// --- Wallet Tools card (left) ---
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET TOOLS");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
|
||||
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
float innerBtnW = colW - pad * 2;
|
||||
if (TactileButton("Address Book...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
||||
AddressBookDialog::show();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (TactileButton("Validate Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
||||
ValidateAddressDialog::show();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (TactileButton("Request Payment...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
||||
RequestPaymentDialog::show();
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(colW, 0));
|
||||
}
|
||||
|
||||
// --- Shielding & Maintenance card (right) ---
|
||||
{
|
||||
float rightX = rowOrigin.x + colW + gap;
|
||||
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SHIELDING & MAINTENANCE");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
|
||||
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
float innerBtnW = colW - pad * 2;
|
||||
if (TactileButton("Shield Mining Rewards...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
||||
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (TactileButton("Merge to Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
||||
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::BeginDisabled(!app->isConnected());
|
||||
if (TactileButton("Rescan Blockchain", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (app->rpc() && app->rpc()->isConnected()) {
|
||||
app->rpc()->rescanBlockchain(0, [](bool success, const nlohmann::json&) {
|
||||
if (success)
|
||||
Notifications::instance().success("Blockchain rescan started");
|
||||
else
|
||||
Notifications::instance().error("Failed to start rescan");
|
||||
});
|
||||
} else {
|
||||
Notifications::instance().warning("Not connected to daemon");
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(colW, 0));
|
||||
}
|
||||
|
||||
// Advance past sidebar row
|
||||
float rowBottom = ImGui::GetCursorScreenPos().y;
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// WALLET INFO — Small card with file path + clear history
|
||||
// ====================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET INFO");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float rowH = body2->LegacySize + Layout::spacingSm();
|
||||
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
||||
float cardH = pad + rowH * 2 + btnRowH + pad;
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
|
||||
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
|
||||
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Location");
|
||||
ImGui::SameLine(labelW);
|
||||
ImGui::TextUnformatted(wallet_path.c_str());
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("File size");
|
||||
ImGui::SameLine(labelW);
|
||||
if (wallet_size > 0) {
|
||||
std::string size_str = util::Platform::formatFileSize(wallet_size);
|
||||
ImGui::TextUnformatted(size_str.c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("Not found");
|
||||
}
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (TactileButton("Clear Z-Transaction History", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||
if (util::Platform::deleteFile(ztx_file))
|
||||
Notifications::instance().success("Z-transaction history cleared");
|
||||
else
|
||||
Notifications::instance().info("No history file found");
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// NODE / RPC — card with two-column inputs
|
||||
// ====================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NODE / RPC CONNECTION");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float rowH = body2->LegacySize + Layout::spacingSm();
|
||||
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
||||
float cardH = pad + rowH * 2 + Layout::spacingSm() + rowH * 2 + Layout::spacingSm()
|
||||
+ capFont->LegacySize + Layout::spacingSm()
|
||||
+ btnRowH + pad;
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
// Two-column: Host+Port on one line, User+Pass on next
|
||||
float halfInput = (availWidth - pad * 2 - labelW * 2 - Layout::spacingLg()) * 0.5f;
|
||||
float rpcLabelW = std::max(70.0f, 85.0f * hs);
|
||||
|
||||
ImGui::PushFont(body2);
|
||||
|
||||
// Row 1: Host + Port
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Host");
|
||||
ImGui::SameLine(rpcLabelW);
|
||||
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
|
||||
ImGui::InputText("##RPCHost", sp_rpc_host, sizeof(sp_rpc_host));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Port");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(std::max(60.0f, halfInput * 0.4f));
|
||||
ImGui::InputText("##RPCPort", sp_rpc_port, sizeof(sp_rpc_port));
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Row 2: Username + Password
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Username");
|
||||
ImGui::SameLine(rpcLabelW);
|
||||
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
|
||||
ImGui::InputText("##RPCUser", sp_rpc_user, sizeof(sp_rpc_user));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Password");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(halfInput);
|
||||
ImGui::InputText("##RPCPassword", sp_rpc_password, sizeof(sp_rpc_password),
|
||||
ImGuiInputTextFlags_Password);
|
||||
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
"Connection settings are usually auto-detected from DRAGONX.conf");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
if (TactileButton("Test Connection", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
if (app->rpc() && app->rpc()->isConnected()) {
|
||||
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
||||
(void)result;
|
||||
if (error.empty())
|
||||
Notifications::instance().success("RPC connection OK");
|
||||
else
|
||||
Notifications::instance().error("RPC error: " + error);
|
||||
});
|
||||
} else {
|
||||
Notifications::instance().warning("Not connected to daemon");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Block Info...", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
BlockInfoDialog::show(app->getBlockHeight());
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// ABOUT — card
|
||||
// ====================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ABOUT");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float rowH = body2->LegacySize + Layout::spacingXs();
|
||||
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
||||
float cardH = pad + sub1->LegacySize + rowH * 2 + Layout::spacingSm()
|
||||
+ body2->LegacySize * 2 + Layout::spacingSm()
|
||||
+ capFont->LegacySize * 2 + Layout::spacingMd()
|
||||
+ btnRowH + pad;
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
||||
|
||||
// App name + version on same line
|
||||
ImGui::PushFont(sub1);
|
||||
ImGui::TextUnformatted(DRAGONX_APP_NAME);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::PushFont(body2);
|
||||
snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION);
|
||||
ImGui::TextUnformatted(buf);
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION);
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
ImGui::PushFont(body2);
|
||||
ImGui::PushTextWrapPos(cardMax.x - pad);
|
||||
ImGui::TextUnformatted(
|
||||
"A shielded cryptocurrency wallet for DragonX (DRGX), "
|
||||
"built with Dear ImGui for a lightweight, portable experience.");
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
ImGui::PushFont(capFont);
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 The Hush Developers | GPLv3 License");
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// Buttons — spread across width
|
||||
{
|
||||
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
|
||||
if (TactileButton("Website", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
util::Platform::openUrl("https://dragonx.is");
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Report Bug", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
util::Platform::openUrl("https://git.hush.is/hush/SilentDragonX/issues");
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
if (TactileButton("Block Explorer", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
ImGui::EndChild(); // ##SettingsPageScroll
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -43,6 +43,11 @@ std::string SkinManager::getBundledSkinsDirectory() {
|
||||
fs::path themes_dir = exe_dir / "res" / "themes";
|
||||
|
||||
if (fs::exists(themes_dir)) {
|
||||
// Update any stale overlay themes from embedded versions
|
||||
int updated = resources::updateBundledThemes(themes_dir.string());
|
||||
if (updated > 0)
|
||||
DEBUG_LOGF("[SkinManager] Updated %d stale theme(s) in %s\n",
|
||||
updated, themes_dir.string().c_str());
|
||||
return themes_dir.string();
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ static constexpr int s_legacyLayoutCount = 10;
|
||||
static std::vector<BalanceLayoutEntry> s_balanceLayouts;
|
||||
static std::string s_defaultLayoutId = "classic";
|
||||
static bool s_layoutConfigLoaded = false;
|
||||
static bool s_generating_z_address = false;
|
||||
|
||||
static void LoadBalanceLayoutConfig()
|
||||
{
|
||||
@@ -803,8 +804,16 @@ static void RenderBalanceClassic(App* app)
|
||||
|
||||
bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
|
||||
ImGui::BeginDisabled(addrSyncing);
|
||||
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
if (s_generating_z_address) {
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
char genLabel[64];
|
||||
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), dotStr[dots]);
|
||||
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
s_generating_z_address = true;
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_z_address = false;
|
||||
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
@@ -1420,9 +1429,18 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
|
||||
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
|
||||
ImGui::BeginDisabled(sharedAddrSyncing);
|
||||
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
|
||||
if (s_generating_z_address) {
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
char genLabel[64];
|
||||
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
|
||||
TactileButton(genLabel, ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
s_generating_z_address = true;
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_z_address = false;
|
||||
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,7 +147,14 @@ void ImportKeyDialog::render(App* app)
|
||||
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) {
|
||||
strncpy(s_key_input, clipboard, sizeof(s_key_input) - 1);
|
||||
std::string trimmed(clipboard);
|
||||
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
|
||||
trimmed.front() == '\n' || trimmed.front() == '\r'))
|
||||
trimmed.erase(trimmed.begin());
|
||||
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
|
||||
trimmed.back() == '\n' || trimmed.back() == '\r'))
|
||||
trimmed.pop_back();
|
||||
snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +163,43 @@ void ImportKeyDialog::render(App* app)
|
||||
s_key_input[0] = '\0';
|
||||
}
|
||||
|
||||
// Key validation indicator
|
||||
if (s_key_input[0] != '\0') {
|
||||
auto keys = splitKeys(s_key_input);
|
||||
int zCount = 0, tCount = 0, unknownCount = 0;
|
||||
for (const auto& key : keys) {
|
||||
std::string kt = detectKeyType(key);
|
||||
if (kt == "z-spending") zCount++;
|
||||
else if (kt == "t-privkey") tCount++;
|
||||
else unknownCount++;
|
||||
}
|
||||
if (zCount > 0 || tCount > 0) {
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Success(), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 2.0f);
|
||||
char validBuf[128];
|
||||
if (zCount > 0 && tCount > 0)
|
||||
snprintf(validBuf, sizeof(validBuf), "%d shielded, %d transparent key(s)", zCount, tCount);
|
||||
else if (zCount > 0)
|
||||
snprintf(validBuf, sizeof(validBuf), "%d shielded key(s)", zCount);
|
||||
else
|
||||
snprintf(validBuf, sizeof(validBuf), "%d transparent key(s)", tCount);
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Success(), validBuf);
|
||||
if (unknownCount > 0) {
|
||||
ImGui::SameLine();
|
||||
snprintf(validBuf, sizeof(validBuf), "(%d unrecognized)", unknownCount);
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Error(), validBuf);
|
||||
}
|
||||
} else if (unknownCount > 0) {
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Error(), ICON_MD_ERROR);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 2.0f);
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Rescan options
|
||||
|
||||
@@ -156,7 +156,9 @@ void RenderMiningTab(App* app)
|
||||
s_pool_state_loaded = true;
|
||||
}
|
||||
|
||||
// Default pool worker to user's first shielded address once addresses are available
|
||||
// Default pool worker to user's first shielded (z) address once available.
|
||||
// For new wallets without a z-address, leave the field blank so the user
|
||||
// is prompted to generate one before mining.
|
||||
{
|
||||
static bool s_pool_worker_defaulted = false;
|
||||
std::string workerStr(s_pool_worker);
|
||||
@@ -169,18 +171,14 @@ void RenderMiningTab(App* app)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultAddr.empty()) {
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||
defaultAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!defaultAddr.empty()) {
|
||||
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
} else {
|
||||
// No z-address yet — clear the placeholder "x" so field shows empty
|
||||
s_pool_worker[0] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
}
|
||||
s_pool_worker_defaulted = true;
|
||||
}
|
||||
@@ -536,7 +534,12 @@ void RenderMiningTab(App* app)
|
||||
s_pool_settings_dirty = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
|
||||
std::string currentWorkerStr(s_pool_worker);
|
||||
if (currentWorkerStr.empty()) {
|
||||
ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint"));
|
||||
} else {
|
||||
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Worker: Dropdown arrow button ---
|
||||
@@ -739,7 +742,8 @@ void RenderMiningTab(App* app)
|
||||
|
||||
if (btnClk) {
|
||||
strncpy(s_pool_url, "pool.dragonx.is", sizeof(s_pool_url) - 1);
|
||||
// Default to user's first shielded address for pool payouts
|
||||
// Default to user's first shielded (z) address for pool payouts.
|
||||
// Leave blank if no z-address exists yet.
|
||||
std::string defaultAddr;
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "shielded" && !addr.address.empty()) {
|
||||
@@ -747,15 +751,6 @@ void RenderMiningTab(App* app)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultAddr.empty()) {
|
||||
// Fallback to transparent if no shielded available
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||
defaultAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
@@ -840,6 +835,7 @@ void RenderMiningTab(App* app)
|
||||
float idleRightEdge = cardMax.x - pad;
|
||||
{
|
||||
bool idleOn = app->settings()->getMineWhenIdle();
|
||||
bool threadScaling = app->settings()->getIdleThreadScaling();
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* idleIcon = ICON_MD_SCHEDULE;
|
||||
float icoH = icoFont->LegacySize;
|
||||
@@ -875,8 +871,40 @@ void RenderMiningTab(App* app)
|
||||
|
||||
idleRightEdge = btnX - 4.0f * dp;
|
||||
|
||||
// Idle delay combo (to the left of the icon when enabled)
|
||||
// Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
|
||||
if (idleOn) {
|
||||
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
|
||||
float sBtnX = idleRightEdge - btnSz;
|
||||
float sBtnY = btnY;
|
||||
|
||||
if (threadScaling) {
|
||||
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
|
||||
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
||||
}
|
||||
|
||||
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
|
||||
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
|
||||
dl->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
|
||||
sIcoCol, scaleIcon);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
|
||||
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
app->settings()->setIdleThreadScaling(!threadScaling);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", threadScaling
|
||||
? TR("mining_idle_scale_on_tooltip")
|
||||
: TR("mining_idle_scale_off_tooltip"));
|
||||
}
|
||||
idleRightEdge = sBtnX - 4.0f * dp;
|
||||
}
|
||||
|
||||
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
|
||||
if (idleOn && !threadScaling) {
|
||||
struct DelayOption { int seconds; const char* label; };
|
||||
static const DelayOption delays[] = {
|
||||
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
||||
@@ -907,6 +935,111 @@ void RenderMiningTab(App* app)
|
||||
idleRightEdge = comboX - 4.0f * dp;
|
||||
}
|
||||
|
||||
// Thread scaling controls: idle delay + active threads / idle threads combos
|
||||
if (idleOn && threadScaling) {
|
||||
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
||||
|
||||
// Idle delay combo
|
||||
{
|
||||
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())
|
||||
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
||||
idleRightEdge = comboX - 4.0f * dp;
|
||||
}
|
||||
|
||||
// Idle threads combo (threads when system is idle)
|
||||
{
|
||||
int curVal = app->settings()->getIdleThreadsIdle();
|
||||
if (curVal <= 0) curVal = hwThreads;
|
||||
char previewBuf[16];
|
||||
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||
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("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
||||
for (int t = 1; t <= hwThreads; t++) {
|
||||
char lbl[16];
|
||||
snprintf(lbl, sizeof(lbl), "%d", t);
|
||||
bool selected = (t == curVal);
|
||||
if (ImGui::Selectable(lbl, selected)) {
|
||||
app->settings()->setIdleThreadsIdle(t);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
|
||||
idleRightEdge = comboX - 4.0f * dp;
|
||||
}
|
||||
|
||||
// Separator arrow icon
|
||||
{
|
||||
const char* arrowIcon = ICON_MD_ARROW_BACK;
|
||||
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
|
||||
float arrX = idleRightEdge - arrSz.x;
|
||||
float arrY = curY + (headerH - arrSz.y) * 0.5f;
|
||||
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
|
||||
idleRightEdge = arrX - 4.0f * dp;
|
||||
}
|
||||
|
||||
// Active threads combo (threads when user is active)
|
||||
{
|
||||
int curVal = app->settings()->getIdleThreadsActive();
|
||||
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
|
||||
char previewBuf[16];
|
||||
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||
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("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
||||
for (int t = 1; t <= hwThreads; t++) {
|
||||
char lbl[16];
|
||||
snprintf(lbl, sizeof(lbl), "%d", t);
|
||||
bool selected = (t == curVal);
|
||||
if (ImGui::Selectable(lbl, selected)) {
|
||||
app->settings()->setIdleThreadsActive(t);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
|
||||
idleRightEdge = comboX - 4.0f * dp;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ static size_t s_prev_address_count = 0;
|
||||
// Address labels (in-memory until persistent config)
|
||||
static std::map<std::string, std::string> s_address_labels;
|
||||
static std::string s_pending_select_address;
|
||||
static bool s_generating_address = false;
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
@@ -302,10 +303,18 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
|
||||
// New address button on same line
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::BeginDisabled(!app->isConnected());
|
||||
if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
|
||||
if (s_generating_address) {
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
char genLabel[64];
|
||||
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), dotStr[dots]);
|
||||
TactileButton(genLabel, 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;
|
||||
if (s_addr_type_filter != 2) {
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_address = false;
|
||||
if (addr.empty())
|
||||
Notifications::instance().error(TR("failed_create_shielded"));
|
||||
else {
|
||||
@@ -315,6 +324,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
});
|
||||
} else {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
s_generating_address = false;
|
||||
if (addr.empty())
|
||||
Notifications::instance().error(TR("failed_create_transparent"));
|
||||
else {
|
||||
|
||||
@@ -1,934 +0,0 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Layout G: QR-Centered Hero
|
||||
// - QR code dominates center as hero element
|
||||
// - Address info wraps around the QR
|
||||
// - Payment request section below QR
|
||||
// - Horizontal address strip at bottom for fast switching
|
||||
|
||||
#include "receive_tab.h"
|
||||
#include "send_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../version.h"
|
||||
#include "../../wallet_state.h"
|
||||
#include "../../ui/widgets/qr_code.h"
|
||||
#include "../sidebar.h"
|
||||
#include "../layout.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../notifications.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
using namespace material;
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
static int s_selected_address_idx = -1;
|
||||
static double s_request_amount = 0.0;
|
||||
static char s_request_memo[256] = "";
|
||||
static std::string s_cached_qr_data;
|
||||
static uintptr_t s_qr_texture = 0;
|
||||
static bool s_payment_request_open = false;
|
||||
|
||||
// Track newly created addresses for NEW badge
|
||||
static std::map<std::string, double> s_new_address_timestamps;
|
||||
static size_t s_prev_address_count = 0;
|
||||
|
||||
// Address labels (in-memory until persistent config)
|
||||
static std::map<std::string, std::string> s_address_labels;
|
||||
static char s_label_edit_buf[64] = "";
|
||||
|
||||
// Address type filter
|
||||
static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 35) {
|
||||
if (addr.length() <= maxLen) return addr;
|
||||
size_t halfLen = (maxLen - 3) / 2;
|
||||
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
|
||||
}
|
||||
|
||||
static void OpenExplorerURL(const std::string& address) {
|
||||
std::string url = "https://explorer.dragonx.com/address/" + address;
|
||||
#ifdef _WIN32
|
||||
std::string cmd = "start \"\" \"" + url + "\"";
|
||||
#elif __APPLE__
|
||||
std::string cmd = "open \"" + url + "\"";
|
||||
#else
|
||||
std::string cmd = "xdg-open \"" + url + "\"";
|
||||
#endif
|
||||
system(cmd.c_str());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sync banner
|
||||
// ============================================================================
|
||||
static void RenderSyncBanner(const WalletState& state) {
|
||||
if (!state.sync.syncing || state.sync.isSynced()) return;
|
||||
|
||||
float syncPct = (state.sync.headers > 0)
|
||||
? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f;
|
||||
char syncBuf[128];
|
||||
snprintf(syncBuf, sizeof(syncBuf),
|
||||
"Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.4f, 0.0f, 0.15f));
|
||||
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, 28),
|
||||
false, ImGuiWindowFlags_NoScrollbar);
|
||||
ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), 6));
|
||||
Type().textColored(TypeStyle::Caption, Warning(), syncBuf);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Track new addresses (detect creations)
|
||||
// ============================================================================
|
||||
static void TrackNewAddresses(const WalletState& state) {
|
||||
if (state.addresses.size() > s_prev_address_count && s_prev_address_count > 0) {
|
||||
for (const auto& a : state.addresses) {
|
||||
if (s_new_address_timestamps.find(a.address) == s_new_address_timestamps.end()) {
|
||||
s_new_address_timestamps[a.address] = ImGui::GetTime();
|
||||
}
|
||||
}
|
||||
} else if (s_prev_address_count == 0) {
|
||||
for (const auto& a : state.addresses) {
|
||||
s_new_address_timestamps[a.address] = 0.0;
|
||||
}
|
||||
}
|
||||
s_prev_address_count = state.addresses.size();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Build sorted address groups
|
||||
// ============================================================================
|
||||
struct AddressGroups {
|
||||
std::vector<int> shielded;
|
||||
std::vector<int> transparent;
|
||||
};
|
||||
|
||||
static AddressGroups BuildSortedAddressGroups(const WalletState& state) {
|
||||
AddressGroups groups;
|
||||
for (int i = 0; i < (int)state.addresses.size(); i++) {
|
||||
if (state.addresses[i].type == "shielded")
|
||||
groups.shielded.push_back(i);
|
||||
else
|
||||
groups.transparent.push_back(i);
|
||||
}
|
||||
std::sort(groups.shielded.begin(), groups.shielded.end(), [&](int a, int b) {
|
||||
return state.addresses[a].balance > state.addresses[b].balance;
|
||||
});
|
||||
std::sort(groups.transparent.begin(), groups.transparent.end(), [&](int a, int b) {
|
||||
return state.addresses[a].balance > state.addresses[b].balance;
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QR Hero — the centerpiece of Layout G
|
||||
// ============================================================================
|
||||
static void RenderQRHero(App* app, ImDrawList* dl, const AddressInfo& addr,
|
||||
float width, float qrSize,
|
||||
const std::string& qr_data,
|
||||
const GlassPanelSpec& glassSpec,
|
||||
const WalletState& state,
|
||||
ImFont* sub1, ImFont* /*body2*/, ImFont* capFont) {
|
||||
char buf[128];
|
||||
bool isZ = addr.type == "shielded";
|
||||
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
|
||||
const char* typeBadge = isZ ? "Shielded" : "Transparent";
|
||||
|
||||
float qrPadding = Layout::spacingLg();
|
||||
float totalQrSize = qrSize + qrPadding * 2;
|
||||
float heroH = totalQrSize + 80.0f; // QR + info below
|
||||
|
||||
ImVec2 heroMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 heroMax(heroMin.x + width, heroMin.y + heroH);
|
||||
GlassPanelSpec heroGlass = glassSpec;
|
||||
heroGlass.fillAlpha = 16;
|
||||
heroGlass.borderAlpha = 35;
|
||||
DrawGlassPanel(dl, heroMin, heroMax, heroGlass);
|
||||
|
||||
// --- Address info bar above QR ---
|
||||
float infoBarH = 32.0f;
|
||||
float cx = heroMin.x + Layout::spacingLg();
|
||||
float cy = heroMin.y + Layout::spacingSm();
|
||||
|
||||
// Type badge circle + label
|
||||
dl->AddCircleFilled(ImVec2(cx + 8, cy + 10), 8.0f, IM_COL32(255, 255, 255, 20));
|
||||
const char* typeChar = isZ ? "Z" : "T";
|
||||
ImVec2 tcSz = sub1->CalcTextSizeA(sub1->LegacySize, 100, 0, typeChar);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(cx + 8 - tcSz.x * 0.5f, cy + 10 - tcSz.y * 0.5f),
|
||||
typeCol, typeChar);
|
||||
|
||||
// Education tooltip on badge
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
ImGui::InvisibleButton("##TypeBadgeHero", ImVec2(22, 22));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
if (isZ) {
|
||||
ImGui::SetTooltip(
|
||||
"Shielded Address (Z)\n"
|
||||
"- Full transaction privacy\n"
|
||||
"- Encrypted sender, receiver, amount\n"
|
||||
"- Supports encrypted memos\n"
|
||||
"- Recommended for privacy");
|
||||
} else {
|
||||
ImGui::SetTooltip(
|
||||
"Transparent Address (T)\n"
|
||||
"- Publicly visible on blockchain\n"
|
||||
"- Similar to Bitcoin addresses\n"
|
||||
"- No memo support\n"
|
||||
"- Use Z addresses for privacy");
|
||||
}
|
||||
}
|
||||
|
||||
// Type label text
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 24, cy + 4), typeCol, typeBadge);
|
||||
|
||||
// Balance right-aligned
|
||||
snprintf(buf, sizeof(buf), "%.8f %s", addr.balance, DRAGONX_TICKER);
|
||||
ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
|
||||
float balX = heroMax.x - balSz.x - Layout::spacingLg();
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(balX, cy + 2), typeCol, buf);
|
||||
|
||||
// USD value
|
||||
if (state.market.price_usd > 0 && addr.balance > 0) {
|
||||
double usd = addr.balance * state.market.price_usd;
|
||||
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(heroMax.x - usdSz.x - Layout::spacingLg(), cy + sub1->LegacySize + 4),
|
||||
OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// --- QR Code centered ---
|
||||
float qrOffset = (width - totalQrSize) * 0.5f;
|
||||
ImVec2 qrPanelMin(heroMin.x + qrOffset, heroMin.y + infoBarH + Layout::spacingSm());
|
||||
ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize);
|
||||
|
||||
// Subtle inner panel for QR
|
||||
GlassPanelSpec qrGlass;
|
||||
qrGlass.rounding = glassSpec.rounding * 0.75f;
|
||||
qrGlass.fillAlpha = 12;
|
||||
qrGlass.borderAlpha = 25;
|
||||
DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
|
||||
if (s_qr_texture) {
|
||||
RenderQRCode(s_qr_texture, qrSize);
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - 50,
|
||||
qrPanelMin.y + totalQrSize * 0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, textPos,
|
||||
OnSurfaceDisabled(), "QR unavailable");
|
||||
}
|
||||
|
||||
// Click QR to copy
|
||||
ImGui::SetCursorScreenPos(qrPanelMin);
|
||||
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Click to copy %s",
|
||||
s_request_amount > 0 ? "payment URI" : "address");
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(qr_data.c_str());
|
||||
Notifications::instance().info(s_request_amount > 0
|
||||
? "Payment URI copied to clipboard"
|
||||
: "Address copied to clipboard");
|
||||
}
|
||||
|
||||
// --- Address strip below QR ---
|
||||
float addrStripY = qrPanelMax.y + Layout::spacingMd();
|
||||
float addrStripX = heroMin.x + Layout::spacingLg();
|
||||
float addrStripW = width - Layout::spacingXxl();
|
||||
|
||||
// Full address (word-wrapped)
|
||||
ImVec2 fullAddrPos(addrStripX, addrStripY);
|
||||
float wrapWidth = addrStripW;
|
||||
ImVec2 addrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
|
||||
wrapWidth, addr.address.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize, fullAddrPos,
|
||||
OnSurface(), addr.address.c_str(), nullptr, wrapWidth);
|
||||
|
||||
// Address click-to-copy overlay
|
||||
ImGui::SetCursorScreenPos(fullAddrPos);
|
||||
ImGui::InvisibleButton("##addrCopyHero", ImVec2(wrapWidth, addrSz.y));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Click to copy address");
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
Notifications::instance().info("Address copied to clipboard");
|
||||
}
|
||||
|
||||
// Action buttons row
|
||||
float btnRowY = addrStripY + addrSz.y + Layout::spacingMd();
|
||||
ImGui::SetCursorScreenPos(ImVec2(addrStripX, btnRowY));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
|
||||
{
|
||||
// Copy — primary (uses global glass style)
|
||||
if (TactileSmallButton("Copy Address##hero", schema::UI().resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
Notifications::instance().info("Address copied to clipboard");
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Explorer
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
||||
if (TactileSmallButton("Explorer##hero", schema::UI().resolveFont("button"))) {
|
||||
OpenExplorerURL(addr.address);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
// Send From
|
||||
if (addr.balance > 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
|
||||
if (TactileSmallButton("Send \xe2\x86\x97##hero", schema::UI().resolveFont("button"))) {
|
||||
SetSendFromAddress(addr.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
// Label editor (inline)
|
||||
ImGui::SameLine(0, Layout::spacingXl());
|
||||
auto lblIt = s_address_labels.find(addr.address);
|
||||
std::string currentLabel = (lblIt != s_address_labels.end()) ? lblIt->second : "";
|
||||
snprintf(s_label_edit_buf, sizeof(s_label_edit_buf), "%s", currentLabel.c_str());
|
||||
ImGui::SetNextItemWidth(std::min(200.0f, addrStripW * 0.3f));
|
||||
if (ImGui::InputTextWithHint("##LabelHero", "Add label...",
|
||||
s_label_edit_buf, sizeof(s_label_edit_buf))) {
|
||||
s_address_labels[addr.address] = std::string(s_label_edit_buf);
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
// Update hero height based on actual content
|
||||
float actualBottom = btnRowY + 24;
|
||||
heroH = actualBottom - heroMin.y + Layout::spacingMd();
|
||||
heroMax.y = heroMin.y + heroH;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(heroMin.x, heroMax.y));
|
||||
ImGui::Dummy(ImVec2(width, 0));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Payment request section (below QR hero)
|
||||
// ============================================================================
|
||||
static void RenderPaymentRequest(ImDrawList* dl, const AddressInfo& addr,
|
||||
float innerW, const GlassPanelSpec& glassSpec,
|
||||
const char* suffix) {
|
||||
auto& S = schema::UI();
|
||||
const float kLabelPos = S.label("tabs.receive", "label-column").position;
|
||||
bool hasMemo = (addr.type == "shielded");
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Compute card height
|
||||
float prCardH = 16.0f + 24.0f + 8.0f + 12.0f;
|
||||
if (hasMemo) prCardH += 24.0f;
|
||||
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
|
||||
ImFont* capF = Type().caption();
|
||||
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
|
||||
innerW - 24, s_cached_qr_data.c_str());
|
||||
prCardH += uriSz.y + 8.0f;
|
||||
}
|
||||
if (s_request_amount > 0) prCardH += 32.0f;
|
||||
if (s_request_amount > 0 || s_request_memo[0]) prCardH += 4.0f;
|
||||
|
||||
ImVec2 prMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 prMax(prMin.x + innerW, prMin.y + prCardH);
|
||||
DrawGlassPanel(dl, prMin, prMax, glassSpec);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(prMin.x + Layout::spacingLg(), prMin.y + Layout::spacingMd()));
|
||||
ImGui::Dummy(ImVec2(0, 0));
|
||||
|
||||
ImGui::Text("Amount:");
|
||||
ImGui::SameLine(kLabelPos);
|
||||
ImGui::SetNextItemWidth(std::max(S.input("tabs.receive", "amount-input").width, innerW * 0.4f));
|
||||
char amtId[32];
|
||||
snprintf(amtId, sizeof(amtId), "##RequestAmount%s", suffix);
|
||||
ImGui::InputDouble(amtId, &s_request_amount, 0.01, 1.0, "%.8f");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", DRAGONX_TICKER);
|
||||
|
||||
if (hasMemo) {
|
||||
ImGui::Text("Memo:");
|
||||
ImGui::SameLine(kLabelPos);
|
||||
ImGui::SetNextItemWidth(innerW - kLabelPos - Layout::spacingXxl());
|
||||
char memoId[32];
|
||||
snprintf(memoId, sizeof(memoId), "##RequestMemo%s", suffix);
|
||||
ImGui::InputText(memoId, s_request_memo, sizeof(s_request_memo));
|
||||
}
|
||||
|
||||
// Live URI preview
|
||||
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImFont* capF = Type().caption();
|
||||
ImVec2 uriPos = ImGui::GetCursorScreenPos();
|
||||
float uriWrapW = innerW - Layout::spacingXxl();
|
||||
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
|
||||
uriWrapW, s_cached_qr_data.c_str());
|
||||
dl->AddText(capF, capF->LegacySize, uriPos,
|
||||
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
|
||||
ImGui::Dummy(ImVec2(uriWrapW, uriSz.y + Layout::spacingSm()));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (s_request_amount > 0) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
|
||||
char copyUriId[64];
|
||||
snprintf(copyUriId, sizeof(copyUriId), "Copy Payment URI%s", suffix);
|
||||
if (TactileButton(copyUriId, ImVec2(innerW - Layout::spacingXxl(), 0), S.resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_cached_qr_data.c_str());
|
||||
Notifications::instance().info("Payment URI copied to clipboard");
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
// Share as text
|
||||
char shareId[32];
|
||||
snprintf(shareId, sizeof(shareId), "Share as Text%s", suffix);
|
||||
if (TactileSmallButton(shareId, S.resolveFont("button"))) {
|
||||
char shareBuf[1024];
|
||||
snprintf(shareBuf, sizeof(shareBuf),
|
||||
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
|
||||
s_request_amount, DRAGONX_TICKER,
|
||||
addr.address.c_str(), s_cached_qr_data.c_str());
|
||||
ImGui::SetClipboardText(shareBuf);
|
||||
Notifications::instance().info("Payment request copied to clipboard");
|
||||
}
|
||||
}
|
||||
if (s_request_amount > 0 || s_request_memo[0]) {
|
||||
ImGui::SameLine();
|
||||
char clearId[32];
|
||||
snprintf(clearId, sizeof(clearId), "Clear%s", suffix);
|
||||
if (TactileSmallButton(clearId, S.resolveFont("button"))) {
|
||||
s_request_amount = 0.0;
|
||||
s_request_memo[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(prMin.x, prMax.y));
|
||||
ImGui::Dummy(ImVec2(innerW, 0));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Recent received transactions for selected address
|
||||
// ============================================================================
|
||||
static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& addr,
|
||||
const WalletState& state, float width,
|
||||
ImFont* capFont) {
|
||||
char buf[128];
|
||||
int recvCount = 0;
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.address == addr.address && tx.type == "receive") recvCount++;
|
||||
}
|
||||
if (recvCount == 0) return;
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
snprintf(buf, sizeof(buf), "RECENT RECEIVED (%d)", std::min(recvCount, 3));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), buf);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
int shown = 0;
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.address != addr.address || tx.type != "receive") continue;
|
||||
if (shown >= 3) break;
|
||||
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
float rH = 22.0f;
|
||||
ImVec2 rMax(rMin.x + width, rMin.y + rH);
|
||||
GlassPanelSpec rsGlass;
|
||||
rsGlass.rounding = Layout::glassRounding() * 0.5f;
|
||||
rsGlass.fillAlpha = 8;
|
||||
DrawGlassPanel(dl, rMin, rMax, rsGlass);
|
||||
|
||||
float rx = rMin.x + Layout::spacingMd();
|
||||
float ry = rMin.y + (rH - capFont->LegacySize) * 0.5f;
|
||||
|
||||
// Arrow indicator
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx, ry),
|
||||
Success(), "\xe2\x86\x90");
|
||||
|
||||
snprintf(buf, sizeof(buf), "+%.8f %s %s %s",
|
||||
tx.amount, DRAGONX_TICKER,
|
||||
tx.getTimeString().c_str(),
|
||||
tx.confirmations < 1 ? "(unconfirmed)" : "");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + 16, ry),
|
||||
tx.confirmations >= 1 ? Success() : Warning(), buf);
|
||||
|
||||
ImGui::Dummy(ImVec2(width, rH));
|
||||
ImGui::Dummy(ImVec2(0, 2));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Horizontal Address Strip — bottom switching bar (Layout G signature)
|
||||
// ============================================================================
|
||||
static void RenderAddressStrip(App* app, ImDrawList* dl, const WalletState& state,
|
||||
float width, float hs,
|
||||
ImFont* /*sub1*/, ImFont* capFont) {
|
||||
char buf[128];
|
||||
|
||||
// Header row with filter and + New button
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR ADDRESSES");
|
||||
|
||||
float btnW = std::max(70.0f, 85.0f * hs);
|
||||
float comboW = std::max(48.0f, 58.0f * hs);
|
||||
ImGui::SameLine(width - btnW - comboW - Layout::spacingMd());
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
|
||||
const char* types[] = { "All", "Z", "T" };
|
||||
ImGui::SetNextItemWidth(comboW);
|
||||
ImGui::Combo("##AddrTypeStrip", &s_addr_type_filter, types, 3);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!app->isConnected());
|
||||
if (TactileButton("+ New##strip", ImVec2(btnW, 0), schema::UI().resolveFont("button"))) {
|
||||
if (s_addr_type_filter != 2) {
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
if (addr.empty())
|
||||
Notifications::instance().error("Failed to create new shielded address");
|
||||
else
|
||||
Notifications::instance().success("New shielded address created");
|
||||
});
|
||||
} else {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
if (addr.empty())
|
||||
Notifications::instance().error("Failed to create new transparent address");
|
||||
else
|
||||
Notifications::instance().success("New transparent address created");
|
||||
});
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
if (!app->isConnected()) {
|
||||
Type().textColored(TypeStyle::Caption, Warning(), "Waiting for connection...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.addresses.empty()) {
|
||||
// Loading skeleton
|
||||
ImVec2 skelPos = ImGui::GetCursorScreenPos();
|
||||
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
|
||||
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
|
||||
for (int sk = 0; sk < 3; sk++) {
|
||||
dl->AddRectFilled(
|
||||
ImVec2(skelPos.x + sk * (130 + 8), skelPos.y),
|
||||
ImVec2(skelPos.x + sk * (130 + 8) + 120, skelPos.y + 56),
|
||||
skelCol, 6.0f);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(width, 60));
|
||||
return;
|
||||
}
|
||||
|
||||
TrackNewAddresses(state);
|
||||
AddressGroups groups = BuildSortedAddressGroups(state);
|
||||
|
||||
// Build filtered list
|
||||
std::vector<int> filteredIdxs;
|
||||
if (s_addr_type_filter != 2)
|
||||
for (int idx : groups.shielded) filteredIdxs.push_back(idx);
|
||||
if (s_addr_type_filter != 1)
|
||||
for (int idx : groups.transparent) filteredIdxs.push_back(idx);
|
||||
|
||||
// Horizontal scrolling strip
|
||||
float cardW = std::max(140.0f, std::min(200.0f, width * 0.22f));
|
||||
float cardH = std::max(52.0f, 64.0f * hs);
|
||||
float stripH = cardH + 8;
|
||||
|
||||
ImGui::BeginChild("##AddrStrip", ImVec2(width, stripH), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoBackground);
|
||||
ImDrawList* sdl = ImGui::GetWindowDrawList();
|
||||
|
||||
for (size_t fi = 0; fi < filteredIdxs.size(); fi++) {
|
||||
int i = filteredIdxs[fi];
|
||||
const auto& addr = state.addresses[i];
|
||||
bool isCurrent = (i == s_selected_address_idx);
|
||||
bool isZ = addr.type == "shielded";
|
||||
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
|
||||
bool hasBalance = addr.balance > 0;
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||
|
||||
// Card background
|
||||
GlassPanelSpec cardGlass;
|
||||
cardGlass.rounding = Layout::glassRounding() * 0.75f;
|
||||
cardGlass.fillAlpha = isCurrent ? 28 : 14;
|
||||
cardGlass.borderAlpha = isCurrent ? 50 : 25;
|
||||
DrawGlassPanel(sdl, cardMin, cardMax, cardGlass);
|
||||
|
||||
// Selected indicator — top accent bar
|
||||
if (isCurrent) {
|
||||
sdl->AddRectFilled(cardMin, ImVec2(cardMax.x, cardMin.y + 3), Primary(),
|
||||
cardGlass.rounding);
|
||||
}
|
||||
|
||||
float ix = cardMin.x + Layout::spacingMd();
|
||||
float iy = cardMin.y + Layout::spacingSm() + (isCurrent ? 4 : 0);
|
||||
|
||||
// Type dot
|
||||
sdl->AddCircleFilled(ImVec2(ix + 4, iy + 6), 3.5f, typeCol);
|
||||
|
||||
// Address label or truncated address
|
||||
auto lblIt = s_address_labels.find(addr.address);
|
||||
bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());
|
||||
size_t addrTruncLen = static_cast<size_t>(std::max(8.0f, (cardW - 30) / 9.0f));
|
||||
|
||||
if (hasLabel) {
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(ix + 14, iy),
|
||||
isCurrent ? PrimaryLight() : OnSurfaceMedium(),
|
||||
lblIt->second.c_str());
|
||||
std::string shortAddr = TruncateAddress(addr.address, std::max((size_t)6, addrTruncLen / 2));
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(ix + 14, iy + capFont->LegacySize + 2),
|
||||
OnSurfaceDisabled(), shortAddr.c_str());
|
||||
} else {
|
||||
std::string dispAddr = TruncateAddress(addr.address, addrTruncLen);
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(ix + 14, iy),
|
||||
isCurrent ? OnSurface() : OnSurfaceDisabled(),
|
||||
dispAddr.c_str());
|
||||
}
|
||||
|
||||
// Balance
|
||||
snprintf(buf, sizeof(buf), "%.4f %s", addr.balance, DRAGONX_TICKER);
|
||||
ImVec2 balSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||
float balY = cardMax.y - balSz.y - Layout::spacingSm();
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(ix + 14, balY),
|
||||
hasBalance ? typeCol : OnSurfaceDisabled(), buf);
|
||||
|
||||
// NEW badge
|
||||
double now = ImGui::GetTime();
|
||||
auto newIt = s_new_address_timestamps.find(addr.address);
|
||||
if (newIt != s_new_address_timestamps.end() && newIt->second > 0.0) {
|
||||
double age = now - newIt->second;
|
||||
if (age < 10.0) {
|
||||
float alpha = (float)std::max(0.0, 1.0 - age / 10.0);
|
||||
int a = (int)(alpha * 220);
|
||||
ImVec2 badgePos(cardMax.x - 32, cardMin.y + 4);
|
||||
sdl->AddRectFilled(badgePos, ImVec2(badgePos.x + 28, badgePos.y + 14),
|
||||
IM_COL32(77, 204, 255, a / 4), 3.0f);
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(badgePos.x + 4, badgePos.y + 1),
|
||||
IM_COL32(77, 204, 255, a), "NEW");
|
||||
}
|
||||
}
|
||||
|
||||
// Click interaction
|
||||
ImGui::SetCursorScreenPos(cardMin);
|
||||
ImGui::PushID(i);
|
||||
ImGui::InvisibleButton("##addrCard", ImVec2(cardW, cardH));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
if (!isCurrent)
|
||||
sdl->AddRectFilled(cardMin, cardMax, IM_COL32(255, 255, 255, 10),
|
||||
cardGlass.rounding);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s\nBalance: %.8f %s%s\nDouble-click to copy | Right-click for options",
|
||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isCurrent ? " (selected)" : "");
|
||||
}
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||
s_selected_address_idx = i;
|
||||
s_cached_qr_data.clear();
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
Notifications::instance().info("Address copied to clipboard");
|
||||
}
|
||||
|
||||
// Context menu
|
||||
if (ImGui::BeginPopupContextItem("##addrStripCtx")) {
|
||||
if (ImGui::MenuItem("Copy Address")) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
Notifications::instance().info("Address copied to clipboard");
|
||||
}
|
||||
if (ImGui::MenuItem("View on Explorer")) {
|
||||
OpenExplorerURL(addr.address);
|
||||
}
|
||||
if (addr.balance > 0) {
|
||||
if (ImGui::MenuItem("Send From This Address")) {
|
||||
SetSendFromAddress(addr.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
}
|
||||
|
||||
// Total balance at end of strip
|
||||
{
|
||||
double totalBal = 0;
|
||||
for (const auto& a : state.addresses) totalBal += a.balance;
|
||||
ImVec2 totPos = ImGui::GetCursorScreenPos();
|
||||
float totCardW = std::max(100.0f, cardW * 0.6f);
|
||||
ImVec2 totMax(totPos.x + totCardW, totPos.y + cardH);
|
||||
|
||||
GlassPanelSpec totGlass;
|
||||
totGlass.rounding = Layout::glassRounding() * 0.75f;
|
||||
totGlass.fillAlpha = 8;
|
||||
totGlass.borderAlpha = 15;
|
||||
DrawGlassPanel(sdl, totPos, totMax, totGlass);
|
||||
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(totPos.x + Layout::spacingMd(), totPos.y + Layout::spacingSm()),
|
||||
OnSurfaceMedium(), "TOTAL");
|
||||
snprintf(buf, sizeof(buf), "%.8f", totalBal);
|
||||
ImVec2 totSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(totPos.x + Layout::spacingMd(),
|
||||
totMax.y - totSz.y - Layout::spacingSm()),
|
||||
OnSurface(), buf);
|
||||
snprintf(buf, sizeof(buf), "%s", DRAGONX_TICKER);
|
||||
sdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(totPos.x + Layout::spacingMd(),
|
||||
totMax.y - totSz.y - Layout::spacingSm() - capFont->LegacySize - 2),
|
||||
OnSurfaceDisabled(), buf);
|
||||
ImGui::Dummy(ImVec2(totCardW, cardH));
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
|
||||
int next = s_selected_address_idx + 1;
|
||||
if (next < (int)state.addresses.size()) {
|
||||
s_selected_address_idx = next;
|
||||
s_cached_qr_data.clear();
|
||||
}
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
|
||||
int prev = s_selected_address_idx - 1;
|
||||
if (prev >= 0) {
|
||||
s_selected_address_idx = prev;
|
||||
s_cached_qr_data.clear();
|
||||
}
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
|
||||
if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
|
||||
ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
|
||||
Notifications::instance().info("Address copied to clipboard");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild(); // ##AddrStrip
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN: RenderReceiveTab — Layout G: QR-Centered Hero
|
||||
// ============================================================================
|
||||
void RenderReceiveTab(App* app)
|
||||
{
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
RenderSyncBanner(state);
|
||||
|
||||
ImVec2 recvAvail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##ReceiveScroll", recvAvail, false, ImGuiWindowFlags_NoBackground);
|
||||
|
||||
float hs = Layout::hScale(recvAvail.x);
|
||||
float vScale = Layout::vScale(recvAvail.y);
|
||||
float glassRound = Layout::glassRounding();
|
||||
|
||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||
float contentWidth = std::min(availWidth * 0.92f, 1200.0f * hs);
|
||||
float offsetX = (availWidth - contentWidth) * 0.5f;
|
||||
if (offsetX > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
|
||||
|
||||
float sectionGap = Layout::spacingXl() * vScale;
|
||||
|
||||
ImGui::BeginGroup();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
GlassPanelSpec glassSpec;
|
||||
glassSpec.rounding = glassRound;
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
ImFont* body2 = Type().body2();
|
||||
|
||||
// Auto-select first address
|
||||
if (!state.addresses.empty() &&
|
||||
(s_selected_address_idx < 0 ||
|
||||
s_selected_address_idx >= (int)state.addresses.size())) {
|
||||
s_selected_address_idx = 0;
|
||||
}
|
||||
|
||||
const AddressInfo* selected = nullptr;
|
||||
if (s_selected_address_idx >= 0 &&
|
||||
s_selected_address_idx < (int)state.addresses.size()) {
|
||||
selected = &state.addresses[s_selected_address_idx];
|
||||
}
|
||||
|
||||
// Generate QR data
|
||||
std::string qr_data;
|
||||
if (selected) {
|
||||
qr_data = selected->address;
|
||||
if (s_request_amount > 0) {
|
||||
qr_data = std::string("dragonx:") + selected->address +
|
||||
"?amount=" + std::to_string(s_request_amount);
|
||||
if (s_request_memo[0] && selected->type == "shielded") {
|
||||
qr_data += "&memo=" + std::string(s_request_memo);
|
||||
}
|
||||
}
|
||||
if (qr_data != s_cached_qr_data) {
|
||||
if (s_qr_texture) {
|
||||
FreeQRTexture(s_qr_texture);
|
||||
s_qr_texture = 0;
|
||||
}
|
||||
int w, h;
|
||||
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
|
||||
s_cached_qr_data = qr_data;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Not connected / empty state
|
||||
// ================================================================
|
||||
if (!app->isConnected()) {
|
||||
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
|
||||
float emptyH = 120.0f;
|
||||
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
|
||||
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
|
||||
OnSurfaceDisabled(), "Waiting for daemon connection...");
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + 8),
|
||||
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
|
||||
ImGui::Dummy(ImVec2(contentWidth, emptyH));
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.addresses.empty()) {
|
||||
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
|
||||
float emptyH = 100.0f;
|
||||
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
|
||||
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
|
||||
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
|
||||
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
|
||||
dl->AddRectFilled(
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
|
||||
ImVec2(emptyMin.x + contentWidth * 0.6f, emptyMin.y + Layout::spacingLg() + 16),
|
||||
skelCol, 4.0f);
|
||||
dl->AddRectFilled(
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + 24),
|
||||
ImVec2(emptyMin.x + contentWidth * 0.4f, emptyMin.y + Layout::spacingLg() + 36),
|
||||
skelCol, 4.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - 24),
|
||||
OnSurfaceDisabled(), "Loading addresses...");
|
||||
ImGui::Dummy(ImVec2(contentWidth, emptyH));
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// QR HERO — dominates center (Layout G signature)
|
||||
// ================================================================
|
||||
if (selected) {
|
||||
// Calculate QR size based on available space
|
||||
float maxQrForWidth = std::min(contentWidth * 0.6f, 400.0f);
|
||||
float maxQrForHeight = std::min(recvAvail.y * 0.45f, 400.0f);
|
||||
float qrSize = std::max(140.0f, std::min(maxQrForWidth, maxQrForHeight));
|
||||
|
||||
// Center the hero horizontally
|
||||
float heroW = std::min(contentWidth, 700.0f * hs);
|
||||
float heroOffsetX = (contentWidth - heroW) * 0.5f;
|
||||
if (heroOffsetX > 4) {
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + heroOffsetX);
|
||||
}
|
||||
|
||||
RenderQRHero(app, dl, *selected, heroW, qrSize, qr_data,
|
||||
glassSpec, state, sub1, body2, capFont);
|
||||
ImGui::Dummy(ImVec2(0, sectionGap));
|
||||
|
||||
// ---- PAYMENT REQUEST (collapsible on narrow) ----
|
||||
constexpr float kTwoColumnThreshold = 800.0f;
|
||||
bool isNarrow = contentWidth < kTwoColumnThreshold;
|
||||
|
||||
if (isNarrow) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1, 1, 1, 0.05f));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1, 1, 1, 0.08f));
|
||||
ImGui::PushFont(Type().overline());
|
||||
s_payment_request_open = ImGui::CollapsingHeader(
|
||||
"PAYMENT REQUEST (OPTIONAL)",
|
||||
s_payment_request_open ? ImGuiTreeNodeFlags_DefaultOpen : 0);
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
if (s_payment_request_open) {
|
||||
float prW = std::min(contentWidth, 600.0f * hs);
|
||||
float prOffX = (contentWidth - prW) * 0.5f;
|
||||
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
|
||||
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
|
||||
}
|
||||
} else {
|
||||
float prW = std::min(contentWidth, 600.0f * hs);
|
||||
float prOffX = (contentWidth - prW) * 0.5f;
|
||||
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
|
||||
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, sectionGap));
|
||||
|
||||
// ---- RECENT RECEIVED ----
|
||||
{
|
||||
float rcvW = std::min(contentWidth, 600.0f * hs);
|
||||
float rcvOffX = (contentWidth - rcvW) * 0.5f;
|
||||
if (rcvOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rcvOffX);
|
||||
RenderRecentReceived(dl, *selected, state, rcvW, capFont);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, sectionGap));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// ADDRESS STRIP — horizontal switching bar at bottom
|
||||
// ================================================================
|
||||
RenderAddressStrip(app, dl, state, contentWidth, hs, sub1, capFont);
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndChild(); // ##ReceiveScroll
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -3,6 +3,7 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "bootstrap.h"
|
||||
#include "../daemon/embedded_daemon.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <miniz.h>
|
||||
@@ -135,11 +136,33 @@ void Bootstrap::start(const std::string& dataDir, const std::string& url) {
|
||||
worker_running_ = false;
|
||||
return;
|
||||
}
|
||||
// Step 3: Clean old chain data
|
||||
// Step 3: Ensure daemon is fully stopped before touching chain data
|
||||
{
|
||||
setProgress(State::Extracting, "Waiting for daemon to stop...");
|
||||
int waited = 0;
|
||||
while (daemon::EmbeddedDaemon::isRpcPortInUse() && waited < 60 && !cancel_requested_) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
waited++;
|
||||
if (waited % 5 == 0)
|
||||
DEBUG_LOGF("[Bootstrap] Still waiting for daemon to stop... (%ds)\n", waited);
|
||||
}
|
||||
if (cancel_requested_) {
|
||||
setProgress(State::Failed, "Cancelled while waiting for daemon");
|
||||
worker_running_ = false;
|
||||
return;
|
||||
}
|
||||
if (daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||
setProgress(State::Failed, "Daemon is still running — stop it before using bootstrap");
|
||||
worker_running_ = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Clean old chain data
|
||||
setProgress(State::Extracting, "Removing old chain data...");
|
||||
cleanChainData(dataDir);
|
||||
|
||||
// Step 4: Extract (skipping wallet.dat)
|
||||
// Step 5: Extract (skipping wallet.dat)
|
||||
if (!extract(zipPath, dataDir)) {
|
||||
if (cancel_requested_)
|
||||
setProgress(State::Failed, "Extraction cancelled");
|
||||
|
||||
@@ -51,6 +51,7 @@ public:
|
||||
|
||||
/// Base URL for bootstrap downloads (zip + checksum files).
|
||||
static constexpr const char* kBaseUrl = "https://bootstrap.dragonx.is";
|
||||
static constexpr const char* kMirrorUrl = "https://bootstrap2.dragonx.is";
|
||||
static constexpr const char* kZipName = "DRAGONX.zip";
|
||||
|
||||
/// Start the bootstrap process on a background thread.
|
||||
|
||||
@@ -297,7 +297,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["tt_noise"] = "Grain texture intensity (0%% = off, 100%% = maximum)";
|
||||
strings_["tt_ui_opacity"] = "Card and sidebar opacity (100%% = fully opaque, lower = more see-through)";
|
||||
strings_["tt_window_opacity"] = "Background opacity (lower = desktop visible through window)";
|
||||
strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x).";
|
||||
strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x). Hotkey: Alt + Scroll Wheel";
|
||||
strings_["tt_custom_theme"] = "Custom theme active";
|
||||
strings_["tt_address_book"] = "Manage saved addresses for quick sending";
|
||||
strings_["tt_validate"] = "Check if a DragonX address is valid";
|
||||
@@ -321,6 +321,12 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["tt_rpc_pass"] = "RPC authentication password";
|
||||
strings_["tt_test_conn"] = "Verify the RPC connection to the daemon";
|
||||
strings_["tt_rescan"] = "Rescan the blockchain for missing transactions";
|
||||
strings_["tt_delete_blockchain"] = "Delete all blockchain data and start a fresh sync. Your wallet.dat and config are preserved.";
|
||||
strings_["delete_blockchain"] = "Delete Blockchain";
|
||||
strings_["delete_blockchain_confirm"] = "Delete & Resync";
|
||||
strings_["confirm_delete_blockchain_title"] = "Delete Blockchain Data";
|
||||
strings_["confirm_delete_blockchain_msg"] = "This will stop the daemon, delete all blockchain data (blocks, chainstate, peers), and start a fresh sync from scratch. This can take several hours to complete.";
|
||||
strings_["confirm_delete_blockchain_safe"] = "Your wallet.dat, config, and transaction history are safe and will not be deleted.";
|
||||
strings_["tt_encrypt"] = "Encrypt wallet.dat with a passphrase";
|
||||
strings_["tt_change_pass"] = "Change the wallet encryption passphrase";
|
||||
strings_["tt_lock"] = "Lock the wallet immediately";
|
||||
@@ -718,6 +724,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["console_daemon_error"] = "Daemon error!";
|
||||
strings_["console_daemon_started"] = "Daemon started";
|
||||
strings_["console_daemon_stopped"] = "Daemon stopped";
|
||||
strings_["daemon_version"] = "Daemon";
|
||||
strings_["console_disconnected"] = "Disconnected from daemon";
|
||||
strings_["console_errors"] = "Errors";
|
||||
strings_["console_filter_hint"] = "Filter output...";
|
||||
@@ -836,6 +843,10 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mining_filter_tip_solo"] = "Show solo earnings only";
|
||||
strings_["mining_idle_off_tooltip"] = "Enable idle mining";
|
||||
strings_["mining_idle_on_tooltip"] = "Disable idle mining";
|
||||
strings_["mining_idle_scale_on_tooltip"] = "Thread scaling: ON\nClick to switch to start/stop mode";
|
||||
strings_["mining_idle_scale_off_tooltip"] = "Start/stop mode: ON\nClick to switch to thread scaling mode";
|
||||
strings_["mining_idle_threads_active_tooltip"] = "Threads when user is active";
|
||||
strings_["mining_idle_threads_idle_tooltip"] = "Threads when system is idle";
|
||||
strings_["mining_local_hashrate"] = "Local Hashrate";
|
||||
strings_["mining_mine"] = "Mine";
|
||||
strings_["mining_mining_addr"] = "Mining Addr";
|
||||
@@ -847,6 +858,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mining_open_in_explorer"] = "Open in explorer";
|
||||
strings_["mining_payout_address"] = "Payout Address";
|
||||
strings_["mining_payout_tooltip"] = "Address to receive mining rewards";
|
||||
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
|
||||
strings_["mining_pool"] = "Pool";
|
||||
strings_["mining_pool_hashrate"] = "Pool Hashrate";
|
||||
strings_["mining_pool_url"] = "Pool URL";
|
||||
@@ -927,6 +939,7 @@ void I18n::loadBuiltinEnglish()
|
||||
// --- Receive Tab ---
|
||||
strings_["click_copy_address"] = "Click to copy address";
|
||||
strings_["click_copy_uri"] = "Click to copy URI";
|
||||
strings_["generating"] = "Generating";
|
||||
strings_["failed_create_shielded"] = "Failed to create shielded address";
|
||||
strings_["failed_create_transparent"] = "Failed to create transparent address";
|
||||
strings_["new_shielded_created"] = "New shielded address created";
|
||||
|
||||
Reference in New Issue
Block a user