feat: Full UI internationalization, pool hashrate stats, and layout caching
- Replace all hardcoded English strings with TR() translation keys across every tab, dialog, and component (~20 UI files) - Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with complete translations (~37k lines added) - Improve i18n loader with exe-relative path fallback and English base fallback for missing keys - Add pool-side hashrate polling via pool stats API in xmrig_manager - Introduce Layout::beginFrame() per-frame caching and refresh balance layout config only on schema generation change - Offload daemon output parsing to worker thread - Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
This commit is contained in:
181
src/app.cpp
181
src/app.cpp
@@ -248,8 +248,18 @@ void App::preFrame()
|
||||
ui::schema::UISchema::instance().applyIfDirty();
|
||||
}
|
||||
|
||||
// Refresh balance layout config after schema reload
|
||||
ui::RefreshBalanceLayoutConfig();
|
||||
// Refresh the per-frame layout cache (reads TOML values once)
|
||||
ui::Layout::beginFrame();
|
||||
|
||||
// Refresh balance layout config only when schema changes
|
||||
{
|
||||
static uint32_t s_balanceLayoutGen = 0;
|
||||
uint32_t gen = ui::schema::UISchema::instance().generation();
|
||||
if (gen != s_balanceLayoutGen) {
|
||||
s_balanceLayoutGen = gen;
|
||||
ui::RefreshBalanceLayoutConfig();
|
||||
}
|
||||
}
|
||||
|
||||
// If font sizes changed in the TOML, rebuild the font atlas
|
||||
if (ui::schema::UISchema::instance().consumeFontsChanged()) {
|
||||
@@ -374,6 +384,7 @@ void App::update()
|
||||
double memMB = xmrig_manager_->getMemoryUsageMB();
|
||||
ps.memory_used = static_cast<int64_t>(memMB * 1024.0 * 1024.0);
|
||||
ps.threads_active = xs.threads_active;
|
||||
ps.pool_hashrate = xs.pool_hashrate;
|
||||
ps.log_lines = xmrig_manager_->getRecentLines(30);
|
||||
|
||||
// Record hashrate sample for the chart
|
||||
@@ -390,91 +401,90 @@ void App::update()
|
||||
state_.mining.log_lines = embedded_daemon_->getRecentLines(50);
|
||||
}
|
||||
|
||||
// Check daemon output for rescan progress
|
||||
// Check daemon output for rescan progress (offloaded to worker)
|
||||
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
||||
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
|
||||
if (!newOutput.empty()) {
|
||||
// Look for rescan progress patterns in new output
|
||||
// Hush patterns: "Still rescanning. At block X. Progress=Y" or "Rescanning..." with percentage
|
||||
bool foundRescan = false;
|
||||
float rescanPct = 0.0f;
|
||||
|
||||
// Search line by line for rescan info
|
||||
size_t pos = 0;
|
||||
while (pos < newOutput.size()) {
|
||||
size_t eol = newOutput.find('\n', pos);
|
||||
if (eol == std::string::npos) eol = newOutput.size();
|
||||
std::string line = newOutput.substr(pos, eol - pos);
|
||||
pos = eol + 1;
|
||||
|
||||
// Check for "Rescanning from height" (rescan starting)
|
||||
if (line.find("Rescanning from height") != std::string::npos ||
|
||||
line.find("Rescanning last") != std::string::npos) {
|
||||
foundRescan = true;
|
||||
state_.sync.rescan_status = line;
|
||||
}
|
||||
|
||||
// Check for "Still rescanning" with progress
|
||||
auto stillIdx = line.find("Still rescanning");
|
||||
if (stillIdx != std::string::npos) {
|
||||
foundRescan = true;
|
||||
// Try to extract progress (Progress=0.XXXX)
|
||||
auto progIdx = line.find("Progress=");
|
||||
if (progIdx != std::string::npos) {
|
||||
size_t numStart = progIdx + 9; // strlen("Progress=")
|
||||
size_t numEnd = numStart;
|
||||
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
|
||||
numEnd++;
|
||||
if (!newOutput.empty() && fast_worker_) {
|
||||
fast_worker_->post([this, output = std::move(newOutput)]() -> rpc::RPCWorker::MainCb {
|
||||
// Parse on worker thread — pure string work, no shared state access
|
||||
bool foundRescan = false;
|
||||
bool finished = false;
|
||||
float rescanPct = 0.0f;
|
||||
std::string lastStatus;
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < output.size()) {
|
||||
size_t eol = output.find('\n', pos);
|
||||
if (eol == std::string::npos) eol = output.size();
|
||||
std::string line = output.substr(pos, eol - pos);
|
||||
pos = eol + 1;
|
||||
|
||||
if (line.find("Rescanning from height") != std::string::npos ||
|
||||
line.find("Rescanning last") != std::string::npos) {
|
||||
foundRescan = true;
|
||||
lastStatus = line;
|
||||
}
|
||||
|
||||
auto stillIdx = line.find("Still rescanning");
|
||||
if (stillIdx != std::string::npos) {
|
||||
foundRescan = true;
|
||||
auto progIdx = line.find("Progress=");
|
||||
if (progIdx != std::string::npos) {
|
||||
size_t numStart = progIdx + 9;
|
||||
size_t numEnd = numStart;
|
||||
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
|
||||
numEnd++;
|
||||
}
|
||||
if (numEnd > numStart) {
|
||||
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
|
||||
}
|
||||
}
|
||||
if (numEnd > numStart) {
|
||||
try {
|
||||
rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f;
|
||||
} catch (...) {}
|
||||
lastStatus = line;
|
||||
}
|
||||
|
||||
auto rescIdx = line.find("Rescanning...");
|
||||
if (rescIdx != std::string::npos) {
|
||||
foundRescan = true;
|
||||
auto pctIdx = line.find('%');
|
||||
if (pctIdx != std::string::npos && pctIdx > 0) {
|
||||
size_t numEnd = pctIdx;
|
||||
size_t numStart = numEnd;
|
||||
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
|
||||
numStart--;
|
||||
}
|
||||
if (numStart < numEnd) {
|
||||
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {}
|
||||
}
|
||||
}
|
||||
lastStatus = line;
|
||||
}
|
||||
|
||||
if (line.find("Done rescanning") != std::string::npos ||
|
||||
line.find("Rescan complete") != std::string::npos) {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Return callback to apply results on main thread
|
||||
return [this, foundRescan, finished, rescanPct, status = std::move(lastStatus)]() {
|
||||
if (finished) {
|
||||
if (state_.sync.rescanning) {
|
||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||
}
|
||||
state_.sync.rescanning = false;
|
||||
state_.sync.rescan_progress = 1.0f;
|
||||
state_.sync.rescan_status.clear();
|
||||
} else if (foundRescan) {
|
||||
state_.sync.rescanning = true;
|
||||
if (rescanPct > 0.0f) {
|
||||
state_.sync.rescan_progress = rescanPct / 100.0f;
|
||||
}
|
||||
if (!status.empty()) {
|
||||
state_.sync.rescan_status = status;
|
||||
}
|
||||
}
|
||||
state_.sync.rescan_status = line;
|
||||
}
|
||||
|
||||
// Check for "Rescanning..." with percentage (ShowProgress output)
|
||||
auto rescIdx = line.find("Rescanning...");
|
||||
if (rescIdx != std::string::npos) {
|
||||
foundRescan = true;
|
||||
// Try to extract percentage
|
||||
auto pctIdx = line.find('%');
|
||||
if (pctIdx != std::string::npos && pctIdx > 0) {
|
||||
// Walk backwards to find the number
|
||||
size_t numEnd = pctIdx;
|
||||
size_t numStart = numEnd;
|
||||
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
|
||||
numStart--;
|
||||
}
|
||||
if (numStart < numEnd) {
|
||||
try {
|
||||
rescanPct = std::stof(line.substr(numStart, numEnd - numStart));
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
state_.sync.rescan_status = line;
|
||||
}
|
||||
|
||||
// Check for "Done rescanning" (rescan complete)
|
||||
if (line.find("Done rescanning") != std::string::npos ||
|
||||
line.find("Rescan complete") != std::string::npos) {
|
||||
if (state_.sync.rescanning) {
|
||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||
}
|
||||
state_.sync.rescanning = false;
|
||||
state_.sync.rescan_progress = 1.0f;
|
||||
state_.sync.rescan_status.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (foundRescan) {
|
||||
state_.sync.rescanning = true;
|
||||
if (rescanPct > 0.0f) {
|
||||
state_.sync.rescan_progress = rescanPct / 100.0f;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
} else if (!embedded_daemon_ || !embedded_daemon_->isRunning()) {
|
||||
// Clear rescan state if daemon is not running (but preserve during restart)
|
||||
@@ -899,11 +909,11 @@ void App::render()
|
||||
if (gradient_tex_ != 0) {
|
||||
sbStatus.gradientTexID = gradient_tex_;
|
||||
}
|
||||
// Count unconfirmed transactions
|
||||
// Count unconfirmed transactions (pending in mempool, not conflicted/orphaned)
|
||||
{
|
||||
int unconf = 0;
|
||||
for (const auto& tx : state_.transactions) {
|
||||
if (!tx.isConfirmed()) ++unconf;
|
||||
if (tx.confirmations == 0) ++unconf;
|
||||
}
|
||||
sbStatus.unconfirmedTxCount = unconf;
|
||||
}
|
||||
@@ -1350,7 +1360,8 @@ void App::renderStatusBar()
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning%s", dotStr);
|
||||
}
|
||||
} else if (state_.sync.syncing) {
|
||||
int blocksLeft = state_.sync.headers - state_.sync.blocks;
|
||||
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
|
||||
int blocksLeft = chainTip - state_.sync.blocks;
|
||||
if (blocksLeft < 0) blocksLeft = 0;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
|
||||
state_.sync.verification_progress * 100.0, blocksLeft);
|
||||
|
||||
@@ -742,9 +742,14 @@ void App::refreshData()
|
||||
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||
if (blockInfo.contains("verificationprogress"))
|
||||
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||
state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2);
|
||||
if (blockInfo.contains("longestchain"))
|
||||
state_.longestchain = blockInfo["longestchain"].get<int>();
|
||||
// Use longestchain (actual network tip) for sync check when available,
|
||||
// since headers can be inflated by misbehaving peers.
|
||||
if (state_.longestchain > 0)
|
||||
state_.sync.syncing = (state_.sync.blocks < state_.longestchain - 2);
|
||||
else
|
||||
state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2);
|
||||
if (blockInfo.contains("notarized"))
|
||||
state_.notarized = blockInfo["notarized"].get<int>();
|
||||
}
|
||||
@@ -886,12 +891,12 @@ void App::refreshBalance()
|
||||
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||
if (blockInfo.contains("verificationprogress"))
|
||||
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||
state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2);
|
||||
|
||||
// Consolidate chain-tip fields that were previously fetched
|
||||
// via a separate getinfo call in refreshMiningInfo.
|
||||
if (blockInfo.contains("longestchain"))
|
||||
state_.longestchain = blockInfo["longestchain"].get<int>();
|
||||
if (state_.longestchain > 0)
|
||||
state_.sync.syncing = (state_.sync.blocks < state_.longestchain - 2);
|
||||
else
|
||||
state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2);
|
||||
if (blockInfo.contains("notarized"))
|
||||
state_.notarized = blockInfo["notarized"].get<int>();
|
||||
}
|
||||
@@ -1182,7 +1187,7 @@ void App::refreshPrice()
|
||||
}
|
||||
|
||||
std::string response_data;
|
||||
const char* url = "https://api.coingecko.com/api/v3/simple/price?ids=hush&vs_currencies=usd,btc&include_24hr_change=true&include_24hr_vol=true&include_market_cap=true";
|
||||
const char* url = "https://api.coingecko.com/api/v3/simple/price?ids=dragonx-2&vs_currencies=usd,btc&include_24hr_change=true&include_24hr_vol=true&include_market_cap=true";
|
||||
|
||||
auto write_callback = [](void* contents, size_t size, size_t nmemb, std::string* userp) -> size_t {
|
||||
size_t totalSize = size * nmemb;
|
||||
@@ -1205,8 +1210,8 @@ void App::refreshPrice()
|
||||
|
||||
if (res == CURLE_OK && http_code == 200) {
|
||||
auto j = json::parse(response_data);
|
||||
if (j.contains("hush")) {
|
||||
const auto& data = j["hush"];
|
||||
if (j.contains("dragonx-2")) {
|
||||
const auto& data = j["dragonx-2"];
|
||||
market.price_usd = data.value("usd", 0.0);
|
||||
market.price_btc = data.value("btc", 0.0);
|
||||
market.change_24h = data.value("usd_24h_change", 0.0);
|
||||
|
||||
@@ -244,6 +244,18 @@ bool XmrigManager::start(const Config& cfg) {
|
||||
}
|
||||
stats_ = PoolStats{};
|
||||
|
||||
// Extract pool hostname for stats API queries
|
||||
{
|
||||
std::string url = cfg.pool_url;
|
||||
// Strip protocol prefix if present
|
||||
auto pos = url.find("://");
|
||||
if (pos != std::string::npos) url = url.substr(pos + 3);
|
||||
// Strip port suffix
|
||||
pos = url.find(':');
|
||||
if (pos != std::string::npos) url = url.substr(0, pos);
|
||||
pool_host_ = url;
|
||||
}
|
||||
|
||||
// Find binary
|
||||
std::string binary = findXmrigBinary();
|
||||
if (binary.empty()) {
|
||||
@@ -572,6 +584,7 @@ void XmrigManager::monitorProcess() {
|
||||
}
|
||||
|
||||
int poll_counter = 0;
|
||||
int pool_api_counter = 0;
|
||||
while (!should_stop_) {
|
||||
drainOutput();
|
||||
|
||||
@@ -605,6 +618,12 @@ void XmrigManager::monitorProcess() {
|
||||
fetchStatsHttp();
|
||||
}
|
||||
|
||||
// Poll pool-side stats every ~30 seconds (300 * 100ms)
|
||||
if (++pool_api_counter >= 300) {
|
||||
pool_api_counter = 0;
|
||||
fetchPoolApiStats();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
drainOutput(); // Final drain
|
||||
@@ -711,5 +730,53 @@ void XmrigManager::fetchStatsHttp() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pool-side stats (hashrate reported by the pool)
|
||||
// ============================================================================
|
||||
|
||||
void XmrigManager::fetchPoolApiStats() {
|
||||
if (state_ != State::Running || pool_host_.empty()) return;
|
||||
|
||||
// Query the pool's public stats API
|
||||
std::string url = "https://" + pool_host_ + "/api/stats";
|
||||
std::string responseData;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) return;
|
||||
|
||||
try {
|
||||
json resp = json::parse(responseData);
|
||||
|
||||
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
|
||||
double poolHR = 0;
|
||||
if (resp.contains("pools") && resp["pools"].is_object()) {
|
||||
for (auto& [key, pool] : resp["pools"].items()) {
|
||||
if (pool.contains("hashrate") && pool["hashrate"].is_number()) {
|
||||
poolHR = pool["hashrate"].get<double>();
|
||||
break; // Use the first pool entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
stats_.pool_hashrate = poolHR;
|
||||
} catch (...) {
|
||||
// Malformed response — ignore
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -49,6 +49,8 @@ public:
|
||||
int64_t memory_total = 0; // bytes
|
||||
int64_t memory_used = 0; // bytes (resident set size)
|
||||
int threads_active = 0; // actual mining threads
|
||||
// Pool-side hashrate (from pool stats API, not xmrig)
|
||||
double pool_hashrate = 0;
|
||||
};
|
||||
|
||||
/// User-facing config (maps 1:1 to UI fields / Settings)
|
||||
@@ -138,6 +140,7 @@ private:
|
||||
void drainOutput();
|
||||
void appendOutput(const char* data, size_t len);
|
||||
void fetchStatsHttp(); // Blocking HTTP call — runs on monitor thread only
|
||||
void fetchPoolApiStats(); // Fetch pool-side stats (hashrate) from pool HTTP API
|
||||
|
||||
std::atomic<State> state_{State::Stopped};
|
||||
std::string last_error_;
|
||||
@@ -149,6 +152,7 @@ private:
|
||||
int api_port_ = 0;
|
||||
std::string api_token_;
|
||||
int threads_ = 0; // Thread count for mining
|
||||
std::string pool_host_; // Pool hostname for stats API
|
||||
PoolStats stats_;
|
||||
mutable std::mutex stats_mutex_;
|
||||
|
||||
|
||||
@@ -11,19 +11,10 @@ const std::vector<ExchangeInfo>& getExchangeRegistry()
|
||||
{
|
||||
static const std::vector<ExchangeInfo> registry = {
|
||||
{
|
||||
"TradeOgre",
|
||||
"https://tradeogre.com",
|
||||
"Nonkyc.io",
|
||||
"https://nonkyc.io",
|
||||
{
|
||||
{"DRGX", "BTC", "DRGX/BTC", "https://tradeogre.com/exchange/DRGX-BTC"},
|
||||
{"DRGX", "LTC", "DRGX/LTC", "https://tradeogre.com/exchange/DRGX-LTC"},
|
||||
{"DRGX", "USDT", "DRGX/USDT", "https://tradeogre.com/exchange/DRGX-USDT"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"Exbitron",
|
||||
"https://www.exbitron.com",
|
||||
{
|
||||
{"DRGX", "USDT", "DRGX/USDT", "https://www.exbitron.com/trading/drgxusdt"},
|
||||
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT"},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -162,6 +162,9 @@ struct PoolMiningState {
|
||||
int64_t memory_used = 0;
|
||||
int threads_active = 0;
|
||||
|
||||
// Pool-side hashrate (from pool stats API)
|
||||
double pool_hashrate = 0;
|
||||
|
||||
// Hashrate history for chart (mirrors MiningInfo::hashrate_history)
|
||||
std::vector<double> hashrate_history;
|
||||
static constexpr int MAX_HISTORY = 60; // 5 minutes at ~5s intervals
|
||||
|
||||
@@ -12,3 +12,4 @@ INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
|
||||
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
|
||||
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
|
||||
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
|
||||
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.otf");
|
||||
|
||||
@@ -26,4 +26,7 @@ extern "C" {
|
||||
|
||||
extern const unsigned char g_material_icons_data[];
|
||||
extern const unsigned int g_material_icons_size;
|
||||
|
||||
extern const unsigned char g_noto_cjk_subset_data[];
|
||||
extern const unsigned int g_noto_cjk_subset_size;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -572,12 +572,12 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
|
||||
float u1 = localX1 / viewportWidth_;
|
||||
float v1 = 1.0f - localY1 / viewportHeight_; // V at bottom-right (low)
|
||||
|
||||
// Draw the blurred background. The glass opacity comes from
|
||||
// fallbackColor.w — blur always renders at full strength.
|
||||
// UI opacity is handled separately by card surface alpha.
|
||||
// Draw the blurred background. Scale by both the glass preset
|
||||
// opacity (fallbackColor.w) AND the user's UI opacity slider so
|
||||
// that lowering card opacity lets the sharp background through.
|
||||
ImTextureID blurTex = getBlurredTexture();
|
||||
uint8_t glassAlpha = static_cast<uint8_t>(
|
||||
std::min(255.0f, std::max(0.0f, params.fallbackColor.w * 255.0f)));
|
||||
std::min(255.0f, std::max(0.0f, params.fallbackColor.w * settings_.uiOpacity * 255.0f)));
|
||||
|
||||
if (blurTex) {
|
||||
drawList->AddImageRounded(
|
||||
@@ -774,11 +774,11 @@ AcrylicFallback AcrylicMaterial::detectFallback() const
|
||||
void AcrylicMaterial::drawTintedRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax,
|
||||
const AcrylicParams& params, float rounding)
|
||||
{
|
||||
// Draw semi-transparent tint without blur
|
||||
// This is the "Tinted Only" fallback mode (not affected by UI opacity)
|
||||
// Draw semi-transparent tint without blur — scale by UI opacity
|
||||
// so the tinted fallback also respects the card transparency slider.
|
||||
ImU32 tintCol = ImGui::ColorConvertFloat4ToU32(
|
||||
ImVec4(params.tintColor.x, params.tintColor.y, params.tintColor.z,
|
||||
params.tintColor.w * params.tintOpacity * 0.9f)
|
||||
params.tintColor.w * params.tintOpacity * 0.9f * settings_.uiOpacity)
|
||||
);
|
||||
drawList->AddRectFilled(pMin, pMax, tintCol, rounding);
|
||||
|
||||
@@ -1528,11 +1528,11 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
|
||||
(void*)blurTex, u0, v0, u1, v1);
|
||||
}
|
||||
|
||||
// Draw the blurred background. Glass opacity from fallbackColor
|
||||
// alpha. Blur always renders at full strength; UI opacity is
|
||||
// handled separately by card surface alpha.
|
||||
// Draw the blurred background. Scale by both the glass preset
|
||||
// opacity (fallbackColor.w) AND the user's UI opacity slider so
|
||||
// that lowering card opacity lets the sharp background through.
|
||||
uint8_t glassAlpha = (uint8_t)std::min(255.f,
|
||||
std::max(0.f, params.fallbackColor.w * 255.f));
|
||||
std::max(0.f, params.fallbackColor.w * settings_.uiOpacity * 255.f));
|
||||
|
||||
if (blurTex) {
|
||||
drawList->AddImageRounded(
|
||||
@@ -1559,7 +1559,7 @@ void AcrylicMaterial::drawTintedRect(ImDrawList* drawList, const ImVec2& pMin, c
|
||||
{
|
||||
ImU32 tintCol = ImGui::ColorConvertFloat4ToU32(
|
||||
ImVec4(params.tintColor.x, params.tintColor.y, params.tintColor.z,
|
||||
params.tintColor.w * params.tintOpacity * 0.9f));
|
||||
params.tintColor.w * params.tintOpacity * 0.9f * settings_.uiOpacity));
|
||||
drawList->AddRectFilled(pMin, pMax, tintCol, rounding);
|
||||
|
||||
// Noise grain overlay — single draw call via pre-tiled texture
|
||||
|
||||
@@ -105,6 +105,12 @@ public:
|
||||
bool hasSandstorm() const { return enabled_ && sandstorm_.enabled; }
|
||||
bool hasViewportOverlay() const { return enabled_ && (viewport_overlay_.colorWashEnabled || viewport_overlay_.vignetteEnabled); }
|
||||
|
||||
/// True when any per-panel visual effect is active (rainbow, shimmer, specular, edge, ember, gradient)
|
||||
bool hasAnyPanelEffect() const {
|
||||
return hasRainbowBorder() || hasShimmer() || hasSpecularGlare()
|
||||
|| hasEdgeTrace() || hasEmberRise() || hasGradientBorder();
|
||||
}
|
||||
|
||||
/// True when any time-dependent effect is active and needs continuous redraws
|
||||
bool hasActiveAnimation() const {
|
||||
if (!enabled_) return false;
|
||||
|
||||
187
src/ui/layout.h
187
src/ui/layout.h
@@ -185,6 +185,102 @@ inline float kItemSpacing() { return schema::UI().drawElement("spacing", "it
|
||||
inline float kLabelValueGap() { return schema::UI().drawElement("spacing", "label-value").sizeOr(4.0f) * dpiScale(); }
|
||||
inline float kSeparatorGap() { return schema::UI().drawElement("spacing", "separator").sizeOr(20.0f) * dpiScale(); }
|
||||
|
||||
// ============================================================================
|
||||
// Per-frame cache — populated once via beginFrame(), avoids repeated
|
||||
// schema hash-map lookups for the hottest accessors.
|
||||
// ============================================================================
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct FrameCache {
|
||||
uint32_t gen = 0; // schema generation when last populated
|
||||
float rawDp = 1.0f; // rawDpiScale() snapshot
|
||||
float dp = 1.0f; // dpiScale() snapshot
|
||||
|
||||
// Spacing tokens (raw, unscaled)
|
||||
float spXs = 2.0f;
|
||||
float spSm = 4.0f;
|
||||
float spMd = 8.0f;
|
||||
float spLg = 12.0f;
|
||||
float spXl = 16.0f;
|
||||
float spXxl = 24.0f;
|
||||
|
||||
// Responsive scale config
|
||||
float refW = 1200.0f;
|
||||
float refH = 700.0f;
|
||||
float minHS = 0.5f;
|
||||
float maxHS = 1.5f;
|
||||
float minVS = 0.5f;
|
||||
float maxVS = 1.4f;
|
||||
float minDen = 0.6f;
|
||||
float maxDen = 1.2f;
|
||||
|
||||
// Breakpoint thresholds (raw, unscaled)
|
||||
float compactW = 500.0f;
|
||||
float compactH = 450.0f;
|
||||
float expandedW = 900.0f;
|
||||
float expandedH = 750.0f;
|
||||
|
||||
// Glass / card helpers (raw, unscaled)
|
||||
float glassRnd = 8.0f;
|
||||
float cardPad = 12.0f;
|
||||
float cardGap = 8.0f;
|
||||
};
|
||||
|
||||
inline FrameCache& frameCache() { static FrameCache c; return c; }
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* @brief Refresh the per-frame layout cache.
|
||||
*
|
||||
* Call once per frame (e.g., from App::preFrame) BEFORE any rendering.
|
||||
* Reads all high-frequency TOML values into fast statics. Invalidated
|
||||
* automatically when the schema generation changes (hot-reload).
|
||||
*/
|
||||
inline void beginFrame() {
|
||||
auto& c = detail::frameCache();
|
||||
uint32_t g = schema::UI().generation();
|
||||
// Also capture DPI each frame (can change on display-scale events)
|
||||
float curRawDp = rawDpiScale();
|
||||
float curDp = dpiScale();
|
||||
if (g == c.gen && curRawDp == c.rawDp && curDp == c.dp) return;
|
||||
c.gen = g;
|
||||
c.rawDp = curRawDp;
|
||||
c.dp = curDp;
|
||||
|
||||
const auto& S = schema::UI();
|
||||
|
||||
// Spacing tokens
|
||||
c.spXs = S.drawElement("spacing-tokens", "xs").sizeOr(2.0f);
|
||||
c.spSm = S.drawElement("spacing-tokens", "sm").sizeOr(4.0f);
|
||||
c.spMd = S.drawElement("spacing-tokens", "md").sizeOr(8.0f);
|
||||
c.spLg = S.drawElement("spacing-tokens", "lg").sizeOr(12.0f);
|
||||
c.spXl = S.drawElement("spacing-tokens", "xl").sizeOr(16.0f);
|
||||
c.spXxl = S.drawElement("spacing-tokens", "xxl").sizeOr(24.0f);
|
||||
|
||||
// Responsive config
|
||||
c.refW = S.drawElement("responsive", "ref-width").sizeOr(1200.0f);
|
||||
c.refH = S.drawElement("responsive", "ref-height").sizeOr(700.0f);
|
||||
c.minHS = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f);
|
||||
c.maxHS = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f);
|
||||
c.minVS = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f);
|
||||
c.maxVS = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
|
||||
c.minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f);
|
||||
c.maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
|
||||
|
||||
// Breakpoints
|
||||
c.compactW = S.drawElement("responsive", "compact-width").sizeOr(500.0f);
|
||||
c.compactH = S.drawElement("responsive", "compact-height").sizeOr(450.0f);
|
||||
c.expandedW = S.drawElement("responsive", "expanded-width").sizeOr(900.0f);
|
||||
c.expandedH = S.drawElement("responsive", "expanded-height").sizeOr(750.0f);
|
||||
|
||||
// Glass / card
|
||||
c.glassRnd = S.drawElement("responsive", "glass-rounding").sizeOr(8.0f);
|
||||
c.cardPad = S.drawElement("responsive", "card-inner-padding").sizeOr(12.0f);
|
||||
c.cardGap = S.drawElement("responsive", "card-gap").sizeOr(8.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Layout Tier (responsive breakpoints)
|
||||
// ============================================================================
|
||||
@@ -203,12 +299,12 @@ enum class LayoutTier { Compact, Normal, Expanded };
|
||||
* Call after ImGui::BeginChild for the content area, or pass explicit avail.
|
||||
*/
|
||||
inline LayoutTier currentTier() {
|
||||
const auto& S = schema::UI();
|
||||
float dp = dpiScale();
|
||||
float cw = S.drawElement("responsive", "compact-width").sizeOr(500.0f) * dp;
|
||||
float ch = S.drawElement("responsive", "compact-height").sizeOr(450.0f) * dp;
|
||||
float ew = S.drawElement("responsive", "expanded-width").sizeOr(900.0f) * dp;
|
||||
float eh = S.drawElement("responsive", "expanded-height").sizeOr(750.0f) * dp;
|
||||
const auto& c = detail::frameCache();
|
||||
float dp = c.dp;
|
||||
float cw = c.compactW * dp;
|
||||
float ch = c.compactH * dp;
|
||||
float ew = c.expandedW * dp;
|
||||
float eh = c.expandedH * dp;
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
if (avail.x < cw || avail.y < ch) return LayoutTier::Compact;
|
||||
if (avail.x > ew && avail.y > eh) return LayoutTier::Expanded;
|
||||
@@ -216,12 +312,12 @@ inline LayoutTier currentTier() {
|
||||
}
|
||||
|
||||
inline LayoutTier currentTier(float availW, float availH) {
|
||||
const auto& S = schema::UI();
|
||||
float dp = dpiScale();
|
||||
float cw = S.drawElement("responsive", "compact-width").sizeOr(500.0f) * dp;
|
||||
float ch = S.drawElement("responsive", "compact-height").sizeOr(450.0f) * dp;
|
||||
float ew = S.drawElement("responsive", "expanded-width").sizeOr(900.0f) * dp;
|
||||
float eh = S.drawElement("responsive", "expanded-height").sizeOr(750.0f) * dp;
|
||||
const auto& c = detail::frameCache();
|
||||
float dp = c.dp;
|
||||
float cw = c.compactW * dp;
|
||||
float ch = c.compactH * dp;
|
||||
float ew = c.expandedW * dp;
|
||||
float eh = c.expandedH * dp;
|
||||
if (availW < cw || availH < ch) return LayoutTier::Compact;
|
||||
if (availW > ew && availH > eh) return LayoutTier::Expanded;
|
||||
return LayoutTier::Normal;
|
||||
@@ -240,15 +336,10 @@ inline LayoutTier currentTier(float availW, float availH) {
|
||||
* while emitting physical-pixel results.
|
||||
*/
|
||||
inline float hScale(float availWidth) {
|
||||
const auto& S = schema::UI();
|
||||
float rawDp = rawDpiScale(); // reference uses hardware DPI only
|
||||
float dp = dpiScale(); // output includes user font scale
|
||||
float rw = S.drawElement("responsive", "ref-width").sizeOr(1200.0f) * rawDp;
|
||||
float minH = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f);
|
||||
float maxH = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f);
|
||||
// Clamp the logical (DPI-neutral) portion, then apply effective DPI.
|
||||
float logical = std::clamp(availWidth / rw, minH, maxH);
|
||||
return logical * dp;
|
||||
const auto& c = detail::frameCache();
|
||||
float rw = c.refW * c.rawDp;
|
||||
float logical = std::clamp(availWidth / rw, c.minHS, c.maxHS);
|
||||
return logical * c.dp;
|
||||
}
|
||||
|
||||
inline float hScale() {
|
||||
@@ -261,14 +352,10 @@ inline float hScale() {
|
||||
* Same decomposition as hScale — logical clamp × DPI.
|
||||
*/
|
||||
inline float vScale(float availHeight) {
|
||||
const auto& S = schema::UI();
|
||||
float rawDp = rawDpiScale(); // reference uses hardware DPI only
|
||||
float dp = dpiScale(); // output includes user font scale
|
||||
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
|
||||
float minV = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f);
|
||||
float maxV = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
|
||||
float logical = std::clamp(availHeight / rh, minV, maxV);
|
||||
return logical * dp;
|
||||
const auto& c = detail::frameCache();
|
||||
float rh = c.refH * c.rawDp;
|
||||
float logical = std::clamp(availHeight / rh, c.minVS, c.maxVS);
|
||||
return logical * c.dp;
|
||||
}
|
||||
|
||||
inline float vScale() {
|
||||
@@ -282,14 +369,10 @@ inline float vScale() {
|
||||
* values scale proportionally with fonts and style.
|
||||
*/
|
||||
inline float densityScale(float availHeight) {
|
||||
const auto& S = schema::UI();
|
||||
float rawDp = rawDpiScale(); // reference uses hardware DPI only
|
||||
float dp = dpiScale(); // output includes user font scale
|
||||
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
|
||||
float minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f);
|
||||
float maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
|
||||
float logical = std::clamp(availHeight / rh, minDen, maxDen);
|
||||
return logical * dp;
|
||||
const auto& c = detail::frameCache();
|
||||
float rh = c.refH * c.rawDp;
|
||||
float logical = std::clamp(availHeight / rh, c.minDen, c.maxDen);
|
||||
return logical * c.dp;
|
||||
}
|
||||
|
||||
inline float densityScale() {
|
||||
@@ -301,33 +384,33 @@ inline float densityScale() {
|
||||
// ============================================================================
|
||||
|
||||
/** @brief Get spacing token scaled by current density. */
|
||||
inline float spacingXs() { return schema::UI().drawElement("spacing-tokens", "xs").sizeOr(2.0f) * densityScale(); }
|
||||
inline float spacingSm() { return schema::UI().drawElement("spacing-tokens", "sm").sizeOr(4.0f) * densityScale(); }
|
||||
inline float spacingMd() { return schema::UI().drawElement("spacing-tokens", "md").sizeOr(8.0f) * densityScale(); }
|
||||
inline float spacingLg() { return schema::UI().drawElement("spacing-tokens", "lg").sizeOr(12.0f) * densityScale(); }
|
||||
inline float spacingXl() { return schema::UI().drawElement("spacing-tokens", "xl").sizeOr(16.0f) * densityScale(); }
|
||||
inline float spacingXxl() { return schema::UI().drawElement("spacing-tokens", "xxl").sizeOr(24.0f) * densityScale(); }
|
||||
inline float spacingXs() { return detail::frameCache().spXs * densityScale(); }
|
||||
inline float spacingSm() { return detail::frameCache().spSm * densityScale(); }
|
||||
inline float spacingMd() { return detail::frameCache().spMd * densityScale(); }
|
||||
inline float spacingLg() { return detail::frameCache().spLg * densityScale(); }
|
||||
inline float spacingXl() { return detail::frameCache().spXl * densityScale(); }
|
||||
inline float spacingXxl() { return detail::frameCache().spXxl * densityScale(); }
|
||||
|
||||
/** @brief Get raw (unscaled) spacing token. */
|
||||
inline float spacingXsRaw() { return schema::UI().drawElement("spacing-tokens", "xs").sizeOr(2.0f); }
|
||||
inline float spacingSmRaw() { return schema::UI().drawElement("spacing-tokens", "sm").sizeOr(4.0f); }
|
||||
inline float spacingMdRaw() { return schema::UI().drawElement("spacing-tokens", "md").sizeOr(8.0f); }
|
||||
inline float spacingLgRaw() { return schema::UI().drawElement("spacing-tokens", "lg").sizeOr(12.0f); }
|
||||
inline float spacingXlRaw() { return schema::UI().drawElement("spacing-tokens", "xl").sizeOr(16.0f); }
|
||||
inline float spacingXxlRaw() { return schema::UI().drawElement("spacing-tokens", "xxl").sizeOr(24.0f); }
|
||||
inline float spacingXsRaw() { return detail::frameCache().spXs; }
|
||||
inline float spacingSmRaw() { return detail::frameCache().spSm; }
|
||||
inline float spacingMdRaw() { return detail::frameCache().spMd; }
|
||||
inline float spacingLgRaw() { return detail::frameCache().spLg; }
|
||||
inline float spacingXlRaw() { return detail::frameCache().spXl; }
|
||||
inline float spacingXxlRaw() { return detail::frameCache().spXxl; }
|
||||
|
||||
// ============================================================================
|
||||
// Responsive Globals Helpers
|
||||
// ============================================================================
|
||||
|
||||
/** @brief Default glass panel rounding (8.0 default). */
|
||||
inline float glassRounding() { return schema::UI().drawElement("responsive", "glass-rounding").sizeOr(8.0f) * dpiScale(); }
|
||||
inline float glassRounding() { return detail::frameCache().glassRnd * dpiScale(); }
|
||||
|
||||
/** @brief Default card inner padding (12.0 default). */
|
||||
inline float cardInnerPadding() { return schema::UI().drawElement("responsive", "card-inner-padding").sizeOr(12.0f) * dpiScale(); }
|
||||
inline float cardInnerPadding() { return detail::frameCache().cardPad * dpiScale(); }
|
||||
|
||||
/** @brief Default card gap (8.0 default). */
|
||||
inline float cardGap() { return schema::UI().drawElement("responsive", "card-gap").sizeOr(8.0f) * dpiScale(); }
|
||||
inline float cardGap() { return detail::frameCache().cardGap * dpiScale(); }
|
||||
|
||||
/**
|
||||
* @brief Compute a responsive card height from a base value.
|
||||
|
||||
@@ -351,18 +351,24 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
|
||||
|
||||
// Noise grain overlay — drawn OVER the surface overlay so card
|
||||
// opacity doesn't hide it. Gives cards a tactile paper feel.
|
||||
// Noise tint is cached per-generation, opacity checked each panel.
|
||||
{
|
||||
float noiseMul = dragonx::ui::effects::ImGuiAcrylic::GetNoiseOpacity();
|
||||
if (noiseMul > 0.0f) {
|
||||
uint8_t origAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF;
|
||||
uint8_t scaledAlpha = static_cast<uint8_t>(std::min(255.0f, origAlpha * noiseMul));
|
||||
// Tint base color changes only on theme reload; opacity slider may change per-frame
|
||||
static uint32_t s_noiseGen = 0;
|
||||
static uint8_t s_baseAlpha = 0;
|
||||
if (curGen != s_noiseGen) {
|
||||
s_noiseGen = curGen;
|
||||
s_baseAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF;
|
||||
}
|
||||
uint8_t scaledAlpha = static_cast<uint8_t>(std::min(255.0f, s_baseAlpha * noiseMul));
|
||||
ImU32 noiseTint = (s_glassNoiseTint & ~(0xFFu << IM_COL32_A_SHIFT)) | (scaledAlpha << IM_COL32_A_SHIFT);
|
||||
float inset = spec.rounding * 0.3f;
|
||||
ImVec2 clipMin(pMin.x + inset, pMin.y + inset);
|
||||
ImVec2 clipMax(pMax.x - inset, pMax.y - inset);
|
||||
dl->PushClipRect(clipMin, clipMax, true);
|
||||
dragonx::util::DrawTiledNoiseRect(dl, clipMin, clipMax, noiseTint);
|
||||
dl->PopClipRect();
|
||||
ImVec2 noiseMin(pMin.x + inset, pMin.y + inset);
|
||||
ImVec2 noiseMax(pMax.x - inset, pMax.y - inset);
|
||||
// Image rect matches clip bounds exactly — no PushClipRect needed
|
||||
dragonx::util::DrawTiledNoiseRect(dl, noiseMin, noiseMax, noiseTint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,18 +381,20 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
|
||||
// Theme visual effects drawn on ForegroundDrawList so they
|
||||
// render above card content (text, values, etc.), not below.
|
||||
auto& fx = effects::ThemeEffects::instance();
|
||||
ImDrawList* fxDl = ImGui::GetForegroundDrawList();
|
||||
if (fx.hasRainbowBorder()) {
|
||||
fx.drawRainbowBorder(fxDl, pMin, pMax, spec.rounding, spec.borderWidth);
|
||||
if (fx.hasAnyPanelEffect()) {
|
||||
ImDrawList* fxDl = ImGui::GetForegroundDrawList();
|
||||
if (fx.hasRainbowBorder()) {
|
||||
fx.drawRainbowBorder(fxDl, pMin, pMax, spec.rounding, spec.borderWidth);
|
||||
}
|
||||
if (fx.hasShimmer()) {
|
||||
fx.drawShimmer(fxDl, pMin, pMax, spec.rounding);
|
||||
}
|
||||
if (fx.hasSpecularGlare()) {
|
||||
fx.drawSpecularGlare(fxDl, pMin, pMax, spec.rounding);
|
||||
}
|
||||
// Per-panel theme effects: edge trace + ember rise
|
||||
fx.drawPanelEffects(fxDl, pMin, pMax, spec.rounding);
|
||||
}
|
||||
if (fx.hasShimmer()) {
|
||||
fx.drawShimmer(fxDl, pMin, pMax, spec.rounding);
|
||||
}
|
||||
if (fx.hasSpecularGlare()) {
|
||||
fx.drawSpecularGlare(fxDl, pMin, pMax, spec.rounding);
|
||||
}
|
||||
// Per-panel theme effects: edge trace + ember rise
|
||||
fx.drawPanelEffects(fxDl, pMin, pMax, spec.rounding);
|
||||
} else {
|
||||
// Low-spec opaque fallback
|
||||
dl->AddRectFilled(pMin, pMax,
|
||||
@@ -749,8 +757,10 @@ inline void ApplySmoothScroll(float speed = 12.0f)
|
||||
s.current = actualY;
|
||||
}
|
||||
|
||||
// Capture mouse wheel when hovered
|
||||
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
|
||||
// Capture mouse wheel when hovered, but not when a popup (combo dropdown
|
||||
// etc.) is open — let the popup handle its own scrolling exclusively.
|
||||
bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
|
||||
if (!popupOpen && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
|
||||
float wheel = ImGui::GetIO().MouseWheel;
|
||||
if (wheel != 0.0f) {
|
||||
float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f;
|
||||
|
||||
@@ -246,11 +246,14 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
cfg.PixelSnapH = true;
|
||||
|
||||
// Include default ASCII + Latin, Latin Extended (for Spanish/multilingual),
|
||||
// plus arrows (⇄ U+21C4), math (≈ U+2248),
|
||||
// general punctuation (— U+2014, … U+2026, etc.)
|
||||
// Cyrillic (for Russian), Greek, plus arrows (⇄ U+21C4),
|
||||
// math (≈ U+2248), general punctuation (— U+2014, … U+2026, etc.)
|
||||
static const ImWchar glyphRanges[] = {
|
||||
0x0020, 0x00FF, // Basic Latin + Latin-1 Supplement
|
||||
0x0100, 0x024F, // Latin Extended-A + Latin Extended-B (Spanish, etc.)
|
||||
0x0370, 0x03FF, // Greek and Coptic
|
||||
0x0400, 0x04FF, // Cyrillic (Russian, Ukrainian, etc.)
|
||||
0x0500, 0x052F, // Cyrillic Supplement
|
||||
0x2000, 0x206F, // General Punctuation (em dash, ellipsis, etc.)
|
||||
0x2190, 0x21FF, // Arrows (includes ⇄ U+21C4, ↻ U+21BB)
|
||||
0x2200, 0x22FF, // Mathematical Operators (includes ≈ U+2248)
|
||||
@@ -269,6 +272,36 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
|
||||
if (font) {
|
||||
DEBUG_LOGF("Typography: Loaded %s (%.0fpx) as '%s'\n", name, size, cfg.Name);
|
||||
|
||||
// Merge CJK fallback glyphs (Chinese/Japanese/Korean) from subset font
|
||||
if (g_noto_cjk_subset_size > 0) {
|
||||
void* cjkCopy = IM_ALLOC(g_noto_cjk_subset_size);
|
||||
memcpy(cjkCopy, g_noto_cjk_subset_data, g_noto_cjk_subset_size);
|
||||
|
||||
ImFontConfig cjkCfg;
|
||||
cjkCfg.FontDataOwnedByAtlas = true;
|
||||
cjkCfg.MergeMode = true; // merge into the font we just loaded
|
||||
cjkCfg.OversampleH = 1;
|
||||
cjkCfg.OversampleV = 1;
|
||||
cjkCfg.PixelSnapH = true;
|
||||
cjkCfg.GlyphMinAdvanceX = 0;
|
||||
// CJK Unified Ideographs + Hiragana + Katakana + Hangul + fullwidth punctuation
|
||||
static const ImWchar cjkRanges[] = {
|
||||
0x2E80, 0x2FDF, // CJK Radicals
|
||||
0x3000, 0x30FF, // CJK Symbols, Hiragana, Katakana
|
||||
0x3100, 0x312F, // Bopomofo
|
||||
0x31F0, 0x31FF, // Katakana Extensions
|
||||
0x3400, 0x4DBF, // CJK Extension A
|
||||
0x4E00, 0x9FFF, // CJK Unified Ideographs
|
||||
0xAC00, 0xD7AF, // Hangul Syllables
|
||||
0xFF00, 0xFFEF, // Fullwidth Forms
|
||||
0,
|
||||
};
|
||||
cjkCfg.GlyphRanges = cjkRanges;
|
||||
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
|
||||
|
||||
io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
||||
IM_FREE(fontDataCopy);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@ bool UISchema::loadFromFile(const std::string& path) {
|
||||
|
||||
// Clear previous data
|
||||
elements_.clear();
|
||||
styleCache_.clear();
|
||||
backgroundImagePath_.clear();
|
||||
logoImagePath_.clear();
|
||||
|
||||
@@ -92,6 +93,7 @@ bool UISchema::loadFromFile(const std::string& path) {
|
||||
overlayPath_.clear(); // No overlay yet
|
||||
loaded_ = true;
|
||||
dirty_ = false;
|
||||
++generation_;
|
||||
|
||||
// Record initial modification time
|
||||
try {
|
||||
@@ -126,6 +128,7 @@ bool UISchema::loadFromString(const std::string& tomlStr, const std::string& lab
|
||||
|
||||
// Clear previous data
|
||||
elements_.clear();
|
||||
styleCache_.clear();
|
||||
backgroundImagePath_.clear();
|
||||
logoImagePath_.clear();
|
||||
|
||||
@@ -222,6 +225,7 @@ bool UISchema::mergeOverlayFromFile(const std::string& path) {
|
||||
// Track overlay file for hot-reload
|
||||
currentPath_ = path;
|
||||
++generation_;
|
||||
styleCache_.clear(); // Invalidate after overlay merges new elements
|
||||
try {
|
||||
lastModTime_ = std::filesystem::last_write_time(path);
|
||||
} catch (const std::filesystem::filesystem_error&) {}
|
||||
@@ -495,6 +499,9 @@ void UISchema::applyIfDirty() {
|
||||
if (!dirty_) return;
|
||||
dirty_ = false;
|
||||
|
||||
// Clear style cache before snapshot so we re-read from TOML
|
||||
styleCache_.clear();
|
||||
|
||||
// Snapshot font sizes before reload for change detection
|
||||
static const char* fontKeys[] = {
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
@@ -513,10 +520,17 @@ void UISchema::applyIfDirty() {
|
||||
|
||||
DEBUG_LOGF("[UISchema] Hot-reload: re-parsing %s\n", currentPath_.c_str());
|
||||
|
||||
// Save overlay path before reloading — loadFromFile/loadFromString clear it
|
||||
std::string savedOverlay = overlayPath_;
|
||||
|
||||
// If an overlay is active, reload base first then re-merge overlay
|
||||
if (!overlayPath_.empty() && !basePath_.empty()) {
|
||||
if (!savedOverlay.empty() && !basePath_.empty()) {
|
||||
loadFromFile(basePath_);
|
||||
mergeOverlayFromFile(overlayPath_);
|
||||
mergeOverlayFromFile(savedOverlay);
|
||||
} else if (!savedOverlay.empty() && !embeddedTomlStr_.empty()) {
|
||||
// Embedded base (e.g. Windows single-file): reload from stored string
|
||||
loadFromString(embeddedTomlStr_, "embedded-reload");
|
||||
mergeOverlayFromFile(savedOverlay);
|
||||
} else {
|
||||
loadFromFile(currentPath_);
|
||||
}
|
||||
@@ -836,15 +850,26 @@ SeparatorStyle UISchema::separator(const std::string& section, const std::string
|
||||
return result;
|
||||
}
|
||||
|
||||
DrawElementStyle UISchema::drawElement(const std::string& section, const std::string& name) const {
|
||||
DrawElementStyle result;
|
||||
const DrawElementStyle& UISchema::drawElement(const std::string& section, const std::string& name) const {
|
||||
static const DrawElementStyle s_empty{};
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
detail::parseDrawElementStyle(elem, result);
|
||||
std::string key = section + "." + name;
|
||||
|
||||
// Return from cache if already parsed
|
||||
auto cit = styleCache_.find(key);
|
||||
if (cit != styleCache_.end()) {
|
||||
return cit->second;
|
||||
}
|
||||
|
||||
return result;
|
||||
// Parse from TOML and cache
|
||||
const void* elem = findElement(section, name);
|
||||
if (!elem) {
|
||||
return s_empty;
|
||||
}
|
||||
|
||||
auto [it, _] = styleCache_.emplace(std::move(key), DrawElementStyle{});
|
||||
detail::parseDrawElementStyle(elem, it->second);
|
||||
return it->second;
|
||||
}
|
||||
|
||||
} // namespace schema
|
||||
|
||||
@@ -255,8 +255,12 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Look up a DrawList custom element style
|
||||
*
|
||||
* Returns a cached const reference. The cache is invalidated on
|
||||
* hot-reload (applyIfDirty) and full loads. For missing elements
|
||||
* a static empty sentinel is returned.
|
||||
*/
|
||||
DrawElementStyle drawElement(const std::string& section, const std::string& name) const;
|
||||
const DrawElementStyle& drawElement(const std::string& section, const std::string& name) const;
|
||||
|
||||
/**
|
||||
* @brief Find a raw stored element by section and name.
|
||||
@@ -365,6 +369,10 @@ private:
|
||||
std::any data; // holds toml::table at runtime
|
||||
};
|
||||
std::unordered_map<std::string, StoredElement> elements_;
|
||||
|
||||
// Parsed DrawElementStyle cache — avoids repeated TOML table iteration.
|
||||
// Populated lazily on first drawElement() lookup, cleared on reload.
|
||||
mutable std::unordered_map<std::string, DrawElementStyle> styleCache_;
|
||||
};
|
||||
|
||||
// Convenience alias
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "layout.h"
|
||||
#include "schema/ui_schema.h"
|
||||
#include "../embedded/IconsMaterialDesign.h"
|
||||
#include "../util/i18n.h"
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
|
||||
@@ -34,25 +35,35 @@ enum class NavPage {
|
||||
};
|
||||
|
||||
struct NavItem {
|
||||
const char* label;
|
||||
const char* label; // fallback label (English)
|
||||
NavPage page;
|
||||
const char* section_label; // if non-null, render section label above this item
|
||||
const char* section_label; // if non-null, render section label above this item
|
||||
const char* tr_key; // i18n key for label
|
||||
const char* section_tr_key; // i18n key for section_label
|
||||
};
|
||||
|
||||
inline const NavItem kNavItems[] = {
|
||||
{ "Overview", NavPage::Overview, nullptr },
|
||||
{ "Send", NavPage::Send, nullptr },
|
||||
{ "Receive", NavPage::Receive, nullptr },
|
||||
{ "History", NavPage::History, nullptr },
|
||||
{ "Mining", NavPage::Mining, "TOOLS" },
|
||||
{ "Market", NavPage::Market, nullptr },
|
||||
{ "Console", NavPage::Console, "ADVANCED" },
|
||||
{ "Network", NavPage::Peers, nullptr },
|
||||
{ "Settings", NavPage::Settings, nullptr },
|
||||
{ "Overview", NavPage::Overview, nullptr, "overview", nullptr },
|
||||
{ "Send", NavPage::Send, nullptr, "send", nullptr },
|
||||
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
|
||||
{ "History", NavPage::History, nullptr, "history", nullptr },
|
||||
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
||||
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
||||
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
||||
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
||||
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
|
||||
};
|
||||
static_assert(sizeof(kNavItems) / sizeof(kNavItems[0]) == (int)NavPage::Count_,
|
||||
"kNavItems must match NavPage::Count_");
|
||||
|
||||
// Get translated nav label at runtime
|
||||
inline const char* NavLabel(const NavItem& item) {
|
||||
return item.tr_key ? TR(item.tr_key) : item.label;
|
||||
}
|
||||
inline const char* NavSectionLabel(const NavItem& item) {
|
||||
return item.section_tr_key ? TR(item.section_tr_key) : item.section_label;
|
||||
}
|
||||
|
||||
// Get the Material Design icon string for a navigation page.
|
||||
inline const char* GetNavIconMD(NavPage page)
|
||||
{
|
||||
@@ -541,7 +552,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
float olFsz = ScaledFontSize(olFont);
|
||||
dl->AddText(olFont, olFsz,
|
||||
ImVec2(wp.x + sbSectionLabelPadLeft, labelY),
|
||||
ImGui::ColorConvertFloat4ToU32(olCol), item.section_label);
|
||||
ImGui::ColorConvertFloat4ToU32(olCol), NavSectionLabel(item));
|
||||
ImGui::Dummy(ImVec2(0, olFsz + 2.0f));
|
||||
} else if (item.section_label && !showLabels) {
|
||||
// Collapsed: thin separator instead of label
|
||||
@@ -609,11 +620,19 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
ImU32 textCol = selected ? Primary() : OnSurfaceMedium();
|
||||
|
||||
if (showLabels) {
|
||||
// Measure total width of icon + gap + label, then center
|
||||
// Measure total width of icon + gap + label, then center.
|
||||
// If the translated label is too wide, shrink the font to fit.
|
||||
ImFont* font = selected ? Type().subtitle2() : Type().body2();
|
||||
float gap = iconLabelGap;
|
||||
float lblFsz = ScaledFontSize(font);
|
||||
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, item.label);
|
||||
float btnW = indMax.x - indMin.x;
|
||||
float maxLabelW = btnW - iconS * 2.0f - gap - Layout::spacingXs() * 2;
|
||||
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
||||
if (labelSz.x > maxLabelW && maxLabelW > 0) {
|
||||
float shrink = maxLabelW / labelSz.x;
|
||||
lblFsz *= shrink;
|
||||
labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
||||
}
|
||||
float totalW = iconS * 2.0f + gap + labelSz.x;
|
||||
float btnCX = (indMin.x + indMax.x) * 0.5f;
|
||||
float startX = btnCX - totalW * 0.5f;
|
||||
@@ -625,7 +644,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
ImVec4 lc = ImGui::ColorConvertU32ToFloat4(textCol);
|
||||
lc.w *= expandFrac;
|
||||
dl->AddText(font, lblFsz, ImVec2(labelX, textY),
|
||||
ImGui::ColorConvertFloat4ToU32(lc), item.label);
|
||||
ImGui::ColorConvertFloat4ToU32(lc), NavLabel(item));
|
||||
} else {
|
||||
float iconCX = (indMin.x + indMax.x) * 0.5f;
|
||||
DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol);
|
||||
@@ -633,7 +652,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
|
||||
// Tooltip when collapsed + hovered
|
||||
if (!showLabels && hovered) {
|
||||
ImGui::SetTooltip("%s", item.label);
|
||||
ImGui::SetTooltip("%s", NavLabel(item));
|
||||
}
|
||||
|
||||
// ---- Badge indicator ----
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "about_dialog.h"
|
||||
#include "../../app.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -23,7 +24,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
auto versionLbl = S.label("dialogs.about", "version-label");
|
||||
auto editionLbl = S.label("dialogs.about", "edition-label");
|
||||
|
||||
if (!material::BeginOverlayDialog("About ObsidianDragon", p_open, win.width, 0.94f)) {
|
||||
if (!material::BeginOverlayDialog(TR("about_title"), p_open, win.width, 0.94f)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,33 +39,33 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - editionLbl.position);
|
||||
ImGui::TextDisabled("ImGui Edition");
|
||||
ImGui::TextDisabled("%s", TR("about_edition"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Version info
|
||||
ImGui::Text("Version:");
|
||||
ImGui::Text("%s", TR("about_version"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::Text("%s", DRAGONX_VERSION);
|
||||
|
||||
ImGui::Text("ImGui:");
|
||||
ImGui::Text("%s", TR("about_imgui"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::Text("%s", IMGUI_VERSION);
|
||||
|
||||
ImGui::Text("Build Date:");
|
||||
ImGui::Text("%s", TR("about_build_date"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::Text("%s %s", __DATE__, __TIME__);
|
||||
|
||||
#ifdef DRAGONX_DEBUG
|
||||
ImGui::Text("Build Type:");
|
||||
ImGui::Text("%s", TR("about_build_type"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Debug");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("about_debug"));
|
||||
#else
|
||||
ImGui::Text("Build Type:");
|
||||
ImGui::Text("%s", TR("about_build_type"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::Text("Release");
|
||||
ImGui::Text("%s", TR("about_release"));
|
||||
#endif
|
||||
|
||||
// Daemon info
|
||||
@@ -73,22 +74,22 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Daemon:");
|
||||
ImGui::Text("%s", TR("about_daemon"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Connected");
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("connected"));
|
||||
|
||||
const auto& state = app->getWalletState();
|
||||
ImGui::Text("Chain:");
|
||||
ImGui::Text("%s", TR("about_chain"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::Text("ObsidianDragon");
|
||||
|
||||
ImGui::Text("Block Height:");
|
||||
ImGui::Text("%s", TR("about_block_height"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::Text("%d", state.sync.blocks);
|
||||
|
||||
ImGui::Text("Connections:");
|
||||
ImGui::Text("%s", TR("about_connections"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::Text("%zu peers", state.peers.size());
|
||||
ImGui::Text(TR("about_peers_count"), state.peers.size());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -96,7 +97,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Credits
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Credits");
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", TR("about_credits"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::BulletText("The Hush Developers");
|
||||
@@ -110,19 +111,16 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
// License
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "License");
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", TR("about_license"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"This software is released under the GNU General Public License v3 (GPLv3). "
|
||||
"You are free to use, modify, and distribute this software under the terms of the license."
|
||||
);
|
||||
ImGui::TextWrapped("%s", TR("about_license_text"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Links
|
||||
if (material::StyledButton("Website", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://dragonx.is");
|
||||
#elif __APPLE__
|
||||
@@ -132,7 +130,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
#endif
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("GitHub", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||
#elif __APPLE__
|
||||
@@ -142,7 +140,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
#endif
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Block Explorer", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://explorer.dragonx.is");
|
||||
#elif __APPLE__
|
||||
@@ -157,7 +155,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
// Close button
|
||||
float button_width = closeBtn.width;
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
|
||||
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
*p_open = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "address_book_dialog.h"
|
||||
#include "../../app.h"
|
||||
#include "../../data/address_book.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../notifications.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -65,11 +66,11 @@ void AddressBookDialog::render(App* app)
|
||||
auto notesInput = S.input("dialogs.address-book", "notes-input");
|
||||
auto actionBtn = S.button("dialogs.address-book", "action-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Address Book", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("address_book_title"), &s_open, win.width, 0.94f)) {
|
||||
auto& book = getAddressBook();
|
||||
|
||||
// Toolbar
|
||||
if (material::StyledButton("Add New", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_add_dialog = true;
|
||||
s_edit_label[0] = '\0';
|
||||
s_edit_address[0] = '\0';
|
||||
@@ -82,7 +83,7 @@ void AddressBookDialog::render(App* app)
|
||||
|
||||
if (!has_selection) ImGui::BeginDisabled();
|
||||
|
||||
if (material::StyledButton("Edit", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
const auto& entry = book.entries()[s_selected_index];
|
||||
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1);
|
||||
@@ -94,20 +95,20 @@ void AddressBookDialog::render(App* app)
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton("Delete", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("delete"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
book.removeEntry(s_selected_index);
|
||||
s_selected_index = -1;
|
||||
Notifications::instance().success("Entry deleted");
|
||||
Notifications::instance().success(TR("address_book_deleted"));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton("Copy Address", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("copy_address"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
ImGui::SetClipboardText(book.entries()[s_selected_index].address.c_str());
|
||||
Notifications::instance().info("Address copied to clipboard");
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +126,16 @@ void AddressBookDialog::render(App* app)
|
||||
{
|
||||
float labelColW = (addrTable.columns.count("label") && addrTable.columns.at("label").width > 0) ? addrTable.columns.at("label").width : 150;
|
||||
float notesColW = (addrTable.columns.count("notes") && addrTable.columns.at("notes").width > 0) ? addrTable.columns.at("notes").width : 150;
|
||||
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, labelColW);
|
||||
ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Notes", ImGuiTableColumnFlags_WidthFixed, notesColW);
|
||||
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthFixed, labelColW);
|
||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthFixed, notesColW);
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
if (book.empty()) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextDisabled("No saved addresses. Click 'Add New' to add one.");
|
||||
ImGui::TextDisabled("%s", TR("address_book_empty"));
|
||||
} else {
|
||||
for (size_t i = 0; i < book.size(); i++) {
|
||||
const auto& entry = book.entries()[i];
|
||||
@@ -182,7 +183,7 @@ void AddressBookDialog::render(App* app)
|
||||
}
|
||||
|
||||
// Status line
|
||||
ImGui::TextDisabled("%zu addresses saved", book.size());
|
||||
ImGui::TextDisabled(TR("address_book_count"), book.size());
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
@@ -196,20 +197,20 @@ void AddressBookDialog::render(App* app)
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
||||
if (ImGui::BeginPopupModal("Add Address", &s_show_add_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
material::Type().text(material::TypeStyle::H6, "Add Address");
|
||||
material::Type().text(material::TypeStyle::H6, TR("address_book_add"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
ImGui::Text("Label:");
|
||||
ImGui::Text("%s", TR("label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##AddLabel", s_edit_label, sizeof(s_edit_label));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Address:");
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##AddAddress", s_edit_address, sizeof(s_edit_address));
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Paste##Add", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) {
|
||||
strncpy(s_edit_address, clipboard, sizeof(s_edit_address) - 1);
|
||||
@@ -218,7 +219,7 @@ void AddressBookDialog::render(App* app)
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Notes (optional):");
|
||||
ImGui::Text("%s", TR("notes_optional"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputTextMultiline("##AddNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
|
||||
|
||||
@@ -229,20 +230,20 @@ void AddressBookDialog::render(App* app)
|
||||
bool can_add = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
|
||||
if (!can_add) ImGui::BeginDisabled();
|
||||
|
||||
if (material::StyledButton("Add", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("add"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
|
||||
if (getAddressBook().addEntry(entry)) {
|
||||
Notifications::instance().success("Address added to book");
|
||||
Notifications::instance().success(TR("address_book_added"));
|
||||
s_show_add_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error("Address already exists in book");
|
||||
Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!can_add) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Cancel", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_add_dialog = false;
|
||||
}
|
||||
|
||||
@@ -257,22 +258,22 @@ void AddressBookDialog::render(App* app)
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
||||
if (ImGui::BeginPopupModal("Edit Address", &s_show_edit_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
material::Type().text(material::TypeStyle::H6, "Edit Address");
|
||||
material::Type().text(material::TypeStyle::H6, TR("address_book_edit"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
ImGui::Text("Label:");
|
||||
ImGui::Text("%s", TR("label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##EditLabel", s_edit_label, sizeof(s_edit_label));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Address:");
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##EditAddress", s_edit_address, sizeof(s_edit_address));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Notes (optional):");
|
||||
ImGui::Text("%s", TR("notes_optional"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputTextMultiline("##EditNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
|
||||
|
||||
@@ -283,20 +284,20 @@ void AddressBookDialog::render(App* app)
|
||||
bool can_save = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
|
||||
if (!can_save) ImGui::BeginDisabled();
|
||||
|
||||
if (material::StyledButton("Save", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("save"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
|
||||
if (getAddressBook().updateEntry(s_selected_index, entry)) {
|
||||
Notifications::instance().success("Address updated");
|
||||
Notifications::instance().success(TR("address_book_updated"));
|
||||
s_show_edit_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error("Failed to update - address may be duplicate");
|
||||
Notifications::instance().error(TR("address_book_update_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!can_save) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Cancel", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_edit_dialog = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,11 +61,8 @@ void BackupWalletDialog::render(App* app)
|
||||
auto backupBtn = S.button("dialogs.backup-wallet", "backup-button");
|
||||
auto closeBtn = S.button("dialogs.backup-wallet", "close-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Backup Wallet", &s_open, win.width, 0.94f)) {
|
||||
ImGui::TextWrapped(
|
||||
"Create a backup of your wallet.dat file. This file contains all your "
|
||||
"private keys and transaction history. Store the backup in a secure location."
|
||||
);
|
||||
if (material::BeginOverlayDialog(TR("backup_title"), &s_open, win.width, 0.94f)) {
|
||||
ImGui::TextWrapped("%s", TR("backup_description"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
@@ -76,7 +73,7 @@ void BackupWalletDialog::render(App* app)
|
||||
}
|
||||
|
||||
// Destination path
|
||||
ImGui::Text("Backup destination:");
|
||||
ImGui::Text("%s", TR("backup_destination"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##Destination", s_destination, sizeof(s_destination));
|
||||
|
||||
@@ -84,13 +81,13 @@ void BackupWalletDialog::render(App* app)
|
||||
|
||||
// Show wallet.dat location
|
||||
std::string walletPath = util::Platform::getDataDir() + "/wallet.dat";
|
||||
ImGui::TextDisabled("Source: %s", walletPath.c_str());
|
||||
ImGui::TextDisabled(TR("backup_source"), walletPath.c_str());
|
||||
|
||||
// Check if source exists
|
||||
bool sourceExists = fs::exists(walletPath);
|
||||
if (!sourceExists) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
ImGui::Text("Warning: wallet.dat not found at expected location");
|
||||
ImGui::Text("%s", TR("backup_wallet_not_found"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
@@ -107,7 +104,7 @@ void BackupWalletDialog::render(App* app)
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton("Create Backup", ImVec2(backupBtn.width, 0), S.resolveFont(backupBtn.font))) {
|
||||
if (material::StyledButton(TR("backup_create"), ImVec2(backupBtn.width, 0), S.resolveFont(backupBtn.font))) {
|
||||
if (strlen(s_destination) == 0) {
|
||||
Notifications::instance().warning("Please enter a destination path");
|
||||
} else if (!app->rpc() || !app->rpc()->isConnected()) {
|
||||
@@ -147,7 +144,7 @@ void BackupWalletDialog::render(App* app)
|
||||
s_status = statusMsg;
|
||||
s_backing_up = false;
|
||||
if (success) {
|
||||
Notifications::instance().success("Wallet backup created");
|
||||
Notifications::instance().success(TR("backup_created"));
|
||||
} else {
|
||||
Notifications::instance().warning(statusMsg);
|
||||
}
|
||||
@@ -160,11 +157,11 @@ void BackupWalletDialog::render(App* app)
|
||||
if (s_backing_up) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Backing up...");
|
||||
ImGui::TextDisabled("%s", TR("backup_backing_up"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
@@ -179,10 +176,10 @@ void BackupWalletDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Tips
|
||||
ImGui::TextDisabled("Tips:");
|
||||
ImGui::BulletText("Store backups on external drives or cloud storage");
|
||||
ImGui::BulletText("Create multiple backups in different locations");
|
||||
ImGui::BulletText("Test restoring from backup periodically");
|
||||
ImGui::TextDisabled("%s", TR("backup_tips"));
|
||||
ImGui::BulletText("%s", TR("backup_tip_external"));
|
||||
ImGui::BulletText("%s", TR("backup_tip_multiple"));
|
||||
ImGui::BulletText("%s", TR("backup_tip_test"));
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
|
||||
static std::string TrId(const char* tr_key, const char* id) {
|
||||
std::string s = TR(tr_key);
|
||||
s += "##";
|
||||
s += id;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Case-insensitive substring search
|
||||
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
|
||||
if (search.empty()) return true;
|
||||
@@ -774,13 +782,13 @@ static void RenderBalanceClassic(App* app)
|
||||
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
|
||||
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox("Hide 0 Balances", &s_hideZeroBalances);
|
||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0").c_str(), &s_hideZeroBalances);
|
||||
{
|
||||
int hc = app->getHiddenAddressCount();
|
||||
if (hc > 0) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
char hlbl[64];
|
||||
snprintf(hlbl, sizeof(hlbl), "Show Hidden (%d)", hc);
|
||||
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
||||
ImGui::Checkbox(hlbl, &s_showHidden);
|
||||
} else {
|
||||
s_showHidden = false;
|
||||
@@ -883,7 +891,8 @@ static void RenderBalanceClassic(App* app)
|
||||
|
||||
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
||||
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
||||
float rowPadLeft = Layout::spacingLg();
|
||||
float rowPadLeft = Layout::cardInnerPadding();
|
||||
float rowPadRight = Layout::cardInnerPadding();
|
||||
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, S.drawElement("tabs.balance", "address-icon-size").size * hs);
|
||||
float innerW = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
@@ -931,10 +940,10 @@ static void RenderBalanceClassic(App* app)
|
||||
// --- Button zone (right edge): [eye] [star] ---
|
||||
float btnH = rowH - Layout::spacingSm() * 2.0f;
|
||||
float btnW = btnH;
|
||||
float btnGap = Layout::spacingXs();
|
||||
float btnGap = Layout::spacingSm();
|
||||
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
|
||||
float rightEdge = rowPos.x + innerW;
|
||||
float starX = rightEdge - btnW - Layout::spacingSm();
|
||||
float starX = rightEdge - btnW - rowPadRight;
|
||||
float eyeX = starX - btnGap - btnW;
|
||||
float btnRound = 6.0f * dp;
|
||||
bool btnClicked = false;
|
||||
@@ -958,7 +967,7 @@ static void RenderBalanceClassic(App* app)
|
||||
else app->favoriteAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? "Remove favorite" : "Favorite address");
|
||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||
}
|
||||
|
||||
// Eye button (zero balance or hidden)
|
||||
@@ -980,7 +989,7 @@ static void RenderBalanceClassic(App* app)
|
||||
else app->hideAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? "Restore address" : "Hide address");
|
||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||
}
|
||||
|
||||
// Content zone ends before buttons
|
||||
@@ -1103,17 +1112,17 @@ static void RenderBalanceClassic(App* app)
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (row.hidden) {
|
||||
if (ImGui::MenuItem("Restore Address"))
|
||||
if (ImGui::MenuItem(TR("restore_address")))
|
||||
app->unhideAddress(addr.address);
|
||||
} else if (addr.balance < 1e-9) {
|
||||
if (ImGui::MenuItem("Hide Address"))
|
||||
if (ImGui::MenuItem(TR("hide_address")))
|
||||
app->hideAddress(addr.address);
|
||||
}
|
||||
if (row.favorite) {
|
||||
if (ImGui::MenuItem("Remove Favorite"))
|
||||
if (ImGui::MenuItem(TR("remove_favorite")))
|
||||
app->unfavoriteAddress(addr.address);
|
||||
} else {
|
||||
if (ImGui::MenuItem("Favorite Address"))
|
||||
if (ImGui::MenuItem(TR("favorite_address")))
|
||||
app->favoriteAddress(addr.address);
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
@@ -1387,13 +1396,13 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
ImGui::SetNextItemWidth(searchW);
|
||||
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox("Hide 0 Balances", &s_hideZeroBalances);
|
||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
|
||||
{
|
||||
int hc = app->getHiddenAddressCount();
|
||||
if (hc > 0) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
char hlbl[64];
|
||||
snprintf(hlbl, sizeof(hlbl), "Show Hidden (%d)", hc);
|
||||
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
||||
ImGui::Checkbox(hlbl, &s_showHidden);
|
||||
} else {
|
||||
s_showHidden = false;
|
||||
@@ -1461,10 +1470,10 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
float ch = ImGui::GetContentRegionAvail().y;
|
||||
if (ch < 60) ch = 60;
|
||||
if (addr_search[0]) {
|
||||
ImVec2 textSz = ImGui::CalcTextSize("No matching addresses");
|
||||
ImVec2 textSz = ImGui::CalcTextSize(TR("no_addresses_match"));
|
||||
ImGui::SetCursorPosX((cw - textSz.x) * 0.5f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
||||
ImGui::TextDisabled("No matching addresses");
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_match"));
|
||||
} else {
|
||||
const char* msg = "No addresses yet";
|
||||
ImVec2 msgSz = ImGui::CalcTextSize(msg);
|
||||
@@ -1482,7 +1491,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
addrScrollMaxY = ImGui::GetScrollMaxY();
|
||||
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
||||
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
||||
float rowPadLeft = Layout::spacingLg();
|
||||
float rowPadLeft = Layout::cardInnerPadding();
|
||||
float rowPadRight = Layout::cardInnerPadding();
|
||||
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
|
||||
S.drawElement("tabs.balance", "address-icon-size").size * hs);
|
||||
float innerW = ImGui::GetContentRegionAvail().x;
|
||||
@@ -1522,10 +1532,10 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
// --- Button zone (right edge): [eye] [star] ---
|
||||
float btnH = rowH - Layout::spacingSm() * 2.0f;
|
||||
float btnW = btnH;
|
||||
float btnGap = Layout::spacingXs();
|
||||
float btnGap = Layout::spacingSm();
|
||||
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
|
||||
float rightEdge = rowPos.x + innerW;
|
||||
float starX = rightEdge - btnW - Layout::spacingSm();
|
||||
float starX = rightEdge - btnW - rowPadRight;
|
||||
float eyeX = starX - btnGap - btnW;
|
||||
float btnRound = 6.0f * dp;
|
||||
bool btnClicked = false;
|
||||
@@ -1549,7 +1559,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
else app->favoriteAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? "Remove favorite" : "Favorite address");
|
||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||
}
|
||||
|
||||
// Eye button (zero balance or hidden)
|
||||
@@ -1571,7 +1581,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
else app->hideAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? "Restore address" : "Hide address");
|
||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||
}
|
||||
|
||||
// Content zone ends before buttons
|
||||
@@ -1672,17 +1682,17 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
QRPopupDialog::show(addr.address, row.isZ ? "Z-Address" : "T-Address");
|
||||
ImGui::Separator();
|
||||
if (row.hidden) {
|
||||
if (ImGui::MenuItem("Restore Address"))
|
||||
if (ImGui::MenuItem(TR("restore_address")))
|
||||
app->unhideAddress(addr.address);
|
||||
} else if (addr.balance < 1e-9) {
|
||||
if (ImGui::MenuItem("Hide Address"))
|
||||
if (ImGui::MenuItem(TR("hide_address")))
|
||||
app->hideAddress(addr.address);
|
||||
}
|
||||
if (row.favorite) {
|
||||
if (ImGui::MenuItem("Remove Favorite"))
|
||||
if (ImGui::MenuItem(TR("remove_favorite")))
|
||||
app->unfavoriteAddress(addr.address);
|
||||
} else {
|
||||
if (ImGui::MenuItem("Favorite Address"))
|
||||
if (ImGui::MenuItem(TR("favorite_address")))
|
||||
app->favoriteAddress(addr.address);
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
|
||||
@@ -99,12 +99,12 @@ void BlockInfoDialog::render(App* app)
|
||||
auto hashBackLbl = S.label("dialogs.block-info", "hash-back-label");
|
||||
auto closeBtn = S.button("dialogs.block-info", "close-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Block Information", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("block_info_title"), &s_open, win.width, 0.94f)) {
|
||||
auto* rpc = app->rpc();
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
// Height input
|
||||
ImGui::Text("Block Height:");
|
||||
ImGui::Text("%s", TR("block_height"));
|
||||
ImGui::SetNextItemWidth(heightInput.width);
|
||||
ImGui::InputInt("##Height", &s_height);
|
||||
if (s_height < 1) s_height = 1;
|
||||
@@ -123,7 +123,7 @@ void BlockInfoDialog::render(App* app)
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton("Get Block Info", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (rpc && rpc->isConnected()) {
|
||||
s_loading = true;
|
||||
s_error.clear();
|
||||
@@ -138,7 +138,7 @@ void BlockInfoDialog::render(App* app)
|
||||
if (s_loading) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Loading...");
|
||||
ImGui::TextDisabled("%s", TR("loading"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -155,23 +155,23 @@ void BlockInfoDialog::render(App* app)
|
||||
// Block info display
|
||||
if (s_has_data) {
|
||||
// Block hash
|
||||
ImGui::Text("Block Hash:");
|
||||
ImGui::Text("%s", TR("block_hash"));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
|
||||
ImGui::TextWrapped("%s", s_block_hash.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Click to copy");
|
||||
ImGui::SetTooltip("%s", TR("click_to_copy"));
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(s_block_hash.c_str());
|
||||
Notifications::instance().success("Block hash copied");
|
||||
Notifications::instance().success(TR("block_hash_copied"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Timestamp
|
||||
ImGui::Text("Timestamp:");
|
||||
ImGui::Text("%s", TR("block_timestamp"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (s_block_time > 0) {
|
||||
std::time_t t = static_cast<std::time_t>(s_block_time);
|
||||
@@ -179,21 +179,21 @@ void BlockInfoDialog::render(App* app)
|
||||
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
ImGui::Text("%s", time_buf);
|
||||
} else {
|
||||
ImGui::TextDisabled("Unknown");
|
||||
ImGui::TextDisabled("%s", TR("unknown"));
|
||||
}
|
||||
|
||||
// Confirmations
|
||||
ImGui::Text("Confirmations:");
|
||||
ImGui::Text("%s", TR("confirmations"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::Text("%d", s_confirmations);
|
||||
|
||||
// Transaction count
|
||||
ImGui::Text("Transactions:");
|
||||
ImGui::Text("%s", TR("block_transactions"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::Text("%d", s_tx_count);
|
||||
|
||||
// Size
|
||||
ImGui::Text("Size:");
|
||||
ImGui::Text("%s", TR("block_size"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (s_block_size > 1024 * 1024) {
|
||||
ImGui::Text("%.2f MB", s_block_size / (1024.0 * 1024.0));
|
||||
@@ -204,12 +204,12 @@ void BlockInfoDialog::render(App* app)
|
||||
}
|
||||
|
||||
// Difficulty
|
||||
ImGui::Text("Difficulty:");
|
||||
ImGui::Text("%s", TR("difficulty"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::Text("%.4f", s_difficulty);
|
||||
|
||||
// Bits
|
||||
ImGui::Text("Bits:");
|
||||
ImGui::Text("%s", TR("block_bits"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::Text("%s", s_bits.c_str());
|
||||
|
||||
@@ -218,7 +218,7 @@ void BlockInfoDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Merkle root
|
||||
ImGui::Text("Merkle Root:");
|
||||
ImGui::Text("%s", TR("block_merkle_root"));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f));
|
||||
ImGui::TextWrapped("%s", s_merkle_root.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
@@ -227,7 +227,7 @@ void BlockInfoDialog::render(App* app)
|
||||
|
||||
// Previous block
|
||||
if (!s_prev_hash.empty()) {
|
||||
ImGui::Text("Previous Block:");
|
||||
ImGui::Text("%s", TR("block_previous"));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
|
||||
|
||||
// Truncate for display
|
||||
@@ -239,7 +239,7 @@ void BlockInfoDialog::render(App* app)
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Click to view previous block");
|
||||
ImGui::SetTooltip("%s", TR("block_click_prev"));
|
||||
}
|
||||
if (ImGui::IsItemClicked() && s_height > 1) {
|
||||
s_height--;
|
||||
@@ -249,7 +249,7 @@ void BlockInfoDialog::render(App* app)
|
||||
|
||||
// Next block
|
||||
if (!s_next_hash.empty()) {
|
||||
ImGui::Text("Next Block:");
|
||||
ImGui::Text("%s", TR("block_next"));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
|
||||
|
||||
// Truncate for display
|
||||
@@ -261,7 +261,7 @@ void BlockInfoDialog::render(App* app)
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Click to view next block");
|
||||
ImGui::SetTooltip("%s", TR("block_click_next"));
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
s_height++;
|
||||
@@ -276,7 +276,7 @@ void BlockInfoDialog::render(App* app)
|
||||
// Navigation buttons
|
||||
if (s_has_data) {
|
||||
if (s_height > 1) {
|
||||
if (material::StyledButton("<< Previous", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
s_height--;
|
||||
s_has_data = false;
|
||||
s_error.clear();
|
||||
@@ -285,7 +285,7 @@ void BlockInfoDialog::render(App* app)
|
||||
}
|
||||
|
||||
if (!s_next_hash.empty()) {
|
||||
if (material::StyledButton("Next >>", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||
s_height++;
|
||||
s_has_data = false;
|
||||
s_error.clear();
|
||||
@@ -295,7 +295,7 @@ void BlockInfoDialog::render(App* app)
|
||||
|
||||
// Close button at bottom
|
||||
ImGui::SetCursorPosY(ImGui::GetWindowHeight() - 40);
|
||||
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "../material/color_theme.h"
|
||||
#include "../theme.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../../util/i18n.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
@@ -75,8 +76,8 @@ ConsoleTab::ConsoleTab()
|
||||
refreshColors();
|
||||
|
||||
// Add welcome message
|
||||
addLine("DragonX Wallet Console", COLOR_INFO);
|
||||
addLine("Type 'help' for available commands", COLOR_INFO);
|
||||
addLine(TR("console_welcome"), COLOR_INFO);
|
||||
addLine(TR("console_type_help"), COLOR_INFO);
|
||||
addLine("", COLOR_RESULT);
|
||||
}
|
||||
|
||||
@@ -117,27 +118,38 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
|
||||
if (current_state == daemon::EmbeddedDaemon::State::Starting &&
|
||||
last_daemon_state_ == daemon::EmbeddedDaemon::State::Stopped) {
|
||||
addLine("", COLOR_RESULT);
|
||||
addLine("=== Starting DragonX Full Node ===", COLOR_INFO);
|
||||
addLine("Capturing daemon output...", COLOR_INFO);
|
||||
addLine(TR("console_starting_node"), COLOR_INFO);
|
||||
addLine(TR("console_capturing_output"), COLOR_INFO);
|
||||
addLine("", COLOR_RESULT);
|
||||
shown_startup_message_ = true;
|
||||
}
|
||||
else if (current_state == daemon::EmbeddedDaemon::State::Running &&
|
||||
last_daemon_state_ != daemon::EmbeddedDaemon::State::Running) {
|
||||
addLine("=== Daemon is running ===", COLOR_INFO);
|
||||
addLine(TR("console_daemon_started"), COLOR_INFO);
|
||||
}
|
||||
else if (current_state == daemon::EmbeddedDaemon::State::Stopped &&
|
||||
last_daemon_state_ == daemon::EmbeddedDaemon::State::Running) {
|
||||
addLine("", COLOR_RESULT);
|
||||
addLine("=== Daemon stopped ===", COLOR_INFO);
|
||||
addLine(TR("console_daemon_stopped"), COLOR_INFO);
|
||||
}
|
||||
else if (current_state == daemon::EmbeddedDaemon::State::Error) {
|
||||
addLine("=== Daemon error: " + daemon->getLastError() + " ===", COLOR_ERROR);
|
||||
addLine(std::string(TR("console_daemon_error")) + daemon->getLastError() + " ===", COLOR_ERROR);
|
||||
}
|
||||
|
||||
last_daemon_state_ = current_state;
|
||||
}
|
||||
|
||||
|
||||
// Track RPC connection state and show a message when connected
|
||||
if (rpc) {
|
||||
bool connected_now = rpc->isConnected();
|
||||
if (connected_now && !last_rpc_connected_) {
|
||||
addLine(TR("console_connected"), COLOR_INFO);
|
||||
} else if (!connected_now && last_rpc_connected_) {
|
||||
addLine(TR("console_disconnected"), COLOR_ERROR);
|
||||
}
|
||||
last_rpc_connected_ = connected_now;
|
||||
}
|
||||
|
||||
// Check for new daemon output — always capture so toggle works as a live filter
|
||||
if (daemon) {
|
||||
std::string new_output = daemon->getOutputSince(last_daemon_output_size_);
|
||||
@@ -372,31 +384,31 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
// Daemon status with colored dot
|
||||
if (daemon) {
|
||||
auto state = daemon->getState();
|
||||
const char* status_text = "Unknown";
|
||||
const char* status_text = TR("console_status_unknown");
|
||||
ImU32 dotCol = IM_COL32(150, 150, 150, 255);
|
||||
bool pulse = false;
|
||||
|
||||
switch (state) {
|
||||
case daemon::EmbeddedDaemon::State::Stopped:
|
||||
status_text = "Stopped";
|
||||
status_text = TR("console_status_stopped");
|
||||
dotCol = IM_COL32(150, 150, 150, 255);
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Starting:
|
||||
status_text = "Starting";
|
||||
status_text = TR("console_status_starting");
|
||||
dotCol = Warning();
|
||||
pulse = true;
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Running:
|
||||
status_text = "Running";
|
||||
status_text = TR("console_status_running");
|
||||
dotCol = Success();
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Stopping:
|
||||
status_text = "Stopping";
|
||||
status_text = TR("console_status_stopping");
|
||||
dotCol = Warning();
|
||||
pulse = true;
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Error:
|
||||
status_text = "Error";
|
||||
status_text = TR("console_status_error");
|
||||
dotCol = Error();
|
||||
break;
|
||||
}
|
||||
@@ -418,7 +430,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
ImGui::SameLine();
|
||||
Type().textColored(TypeStyle::Caption, dotCol, status_text);
|
||||
} else {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No daemon");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -426,7 +438,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
ImGui::SameLine();
|
||||
|
||||
// Auto-scroll toggle
|
||||
if (ImGui::Checkbox("Auto-scroll", &auto_scroll_)) {
|
||||
if (ImGui::Checkbox(TR("console_auto_scroll"), &auto_scroll_)) {
|
||||
if (auto_scroll_) {
|
||||
scroll_to_bottom_ = true;
|
||||
new_lines_since_scroll_ = 0;
|
||||
@@ -440,7 +452,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
// Daemon messages toggle
|
||||
{
|
||||
static bool s_prev_daemon_enabled = true;
|
||||
ImGui::Checkbox("Daemon", &s_daemon_messages_enabled);
|
||||
ImGui::Checkbox(TR("console_daemon"), &s_daemon_messages_enabled);
|
||||
// When toggling daemon filter while auto-scroll is active, scroll to bottom
|
||||
if (s_prev_daemon_enabled != s_daemon_messages_enabled && auto_scroll_) {
|
||||
scroll_to_bottom_ = true;
|
||||
@@ -448,7 +460,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
s_prev_daemon_enabled = s_daemon_messages_enabled;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Show daemon output messages");
|
||||
ImGui::SetTooltip("%s", TR("console_show_daemon_output"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -458,7 +470,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
// Errors-only toggle
|
||||
{
|
||||
static bool s_prev_errors_only = false;
|
||||
ImGui::Checkbox("Errors", &s_errors_only_enabled);
|
||||
ImGui::Checkbox(TR("console_errors"), &s_errors_only_enabled);
|
||||
// When toggling errors filter while auto-scroll is active, scroll to bottom
|
||||
if (s_prev_errors_only != s_errors_only_enabled && auto_scroll_) {
|
||||
scroll_to_bottom_ = true;
|
||||
@@ -466,7 +478,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
s_prev_errors_only = s_errors_only_enabled;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Show only error messages");
|
||||
ImGui::SetTooltip("%s", TR("console_show_errors_only"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -474,7 +486,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
ImGui::SameLine();
|
||||
|
||||
// Clear button
|
||||
if (TactileButton("Clear", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
clear();
|
||||
clearSelection();
|
||||
}
|
||||
@@ -482,7 +494,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
ImGui::SameLine();
|
||||
|
||||
// Copy button — material styled
|
||||
if (TactileButton("Copy", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TR("copy"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
std::lock_guard<std::mutex> lock(lines_mutex_);
|
||||
if (has_selection_) {
|
||||
std::string selected = getSelectedText();
|
||||
@@ -501,17 +513,17 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(has_selection_ ? "Copy selected text" : "Copy all output");
|
||||
ImGui::SetTooltip("%s", has_selection_ ? TR("console_copy_selected") : TR("console_copy_all"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Commands reference button
|
||||
if (TactileButton("Commands", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||
show_commands_popup_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Show RPC command reference");
|
||||
ImGui::SetTooltip("%s", TR("console_show_rpc_ref"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -519,7 +531,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
// Line count
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lines_mutex_);
|
||||
ImGui::TextDisabled("(%zu lines)", lines_.size());
|
||||
ImGui::TextDisabled(TR("console_line_count"), lines_.size());
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -531,7 +543,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
float filterAvail = ImGui::GetContentRegionAvail().x - zoomBtnSpace;
|
||||
float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
|
||||
ImGui::SetNextItemWidth(filterW);
|
||||
ImGui::InputTextWithHint("##ConsoleFilter", "Filter output...", filter_text_, sizeof(filter_text_));
|
||||
ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));
|
||||
|
||||
// Zoom +/- buttons (right side of toolbar)
|
||||
ImGui::SameLine();
|
||||
@@ -548,14 +560,14 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Zoom out (%.0f%%)", s_console_zoom * 100.0f);
|
||||
ImGui::SetTooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
|
||||
s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Zoom in (%.0f%%)", s_console_zoom * 100.0f);
|
||||
ImGui::SetTooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -889,7 +901,7 @@ void ConsoleTab::renderOutput()
|
||||
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
|
||||
if (has_text_filter) {
|
||||
char filterBuf[128];
|
||||
snprintf(filterBuf, sizeof(filterBuf), "Showing %d of %zu lines",
|
||||
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
|
||||
visible_count, lines_.size());
|
||||
ImVec2 indicatorPos = ImGui::GetCursorScreenPos();
|
||||
ImGui::GetWindowDrawList()->AddText(indicatorPos,
|
||||
@@ -899,13 +911,13 @@ void ConsoleTab::renderOutput()
|
||||
|
||||
// Right-click context menu
|
||||
if (ImGui::BeginPopupContextWindow("ConsoleContextMenu")) {
|
||||
if (ImGui::MenuItem("Copy", "Ctrl+C", false, has_selection_)) {
|
||||
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, has_selection_)) {
|
||||
std::string selected = getSelectedText();
|
||||
if (!selected.empty()) {
|
||||
ImGui::SetClipboardText(selected.c_str());
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Select All", "Ctrl+A")) {
|
||||
if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
|
||||
if (!lines_.empty()) {
|
||||
sel_anchor_ = {0, 0};
|
||||
sel_end_ = {static_cast<int>(lines_.size()) - 1,
|
||||
@@ -914,7 +926,7 @@ void ConsoleTab::renderOutput()
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Clear Console")) {
|
||||
if (ImGui::MenuItem(TR("console_clear_console"))) {
|
||||
// Can't call clear() here (already holding lock), just mark for clearing
|
||||
lines_.clear();
|
||||
has_selection_ = false;
|
||||
@@ -938,7 +950,7 @@ void ConsoleTab::renderOutput()
|
||||
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
|
||||
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), " %d new line%s",
|
||||
snprintf(buf, sizeof(buf), TR("console_new_lines"),
|
||||
new_lines_since_scroll_, new_lines_since_scroll_ != 1 ? "s" : "");
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
@@ -1229,7 +1241,7 @@ void ConsoleTab::renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker)
|
||||
data->InsertChars(0, matches[0]);
|
||||
} else if (matches.size() > 1) {
|
||||
// Multiple matches — show list in console and complete common prefix
|
||||
console->addLine("Completions:", ConsoleTab::COLOR_INFO);
|
||||
console->addLine(TR("console_completions"), ConsoleTab::COLOR_INFO);
|
||||
std::string line = " ";
|
||||
for (size_t m = 0; m < matches.size(); m++) {
|
||||
if (m > 0) line += " ";
|
||||
@@ -1284,17 +1296,17 @@ void ConsoleTab::renderCommandsPopup()
|
||||
using namespace material;
|
||||
|
||||
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
|
||||
if (!material::BeginOverlayDialog("RPC Command Reference", &show_commands_popup_, popW, 0.94f)) {
|
||||
if (!material::BeginOverlayDialog(TR("console_rpc_reference"), &show_commands_popup_, popW, 0.94f)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Type().text(TypeStyle::H6, "RPC Command Reference");
|
||||
Type().text(TypeStyle::H6, TR("console_rpc_reference"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Search filter
|
||||
static char cmdFilter[128] = {0};
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##CmdSearch", "Search commands...", cmdFilter, sizeof(cmdFilter));
|
||||
ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), cmdFilter, sizeof(cmdFilter));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Command entries
|
||||
@@ -1507,9 +1519,9 @@ void ConsoleTab::renderCommandsPopup()
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (cmd.params[0] != '\0')
|
||||
ImGui::SetTooltip("Click to insert '%s %s' into input", cmd.name, cmd.params);
|
||||
ImGui::SetTooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
|
||||
else
|
||||
ImGui::SetTooltip("Click to insert '%s' into input", cmd.name);
|
||||
ImGui::SetTooltip(TR("console_click_insert"), cmd.name);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
if (showParams) {
|
||||
@@ -1575,7 +1587,7 @@ void ConsoleTab::renderCommandsPopup()
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Close button
|
||||
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
||||
if (ImGui::Button(TR("console_close"), ImVec2(-1, 0))) {
|
||||
cmdFilter[0] = '\0';
|
||||
show_commands_popup_ = false;
|
||||
}
|
||||
@@ -1605,28 +1617,28 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
}
|
||||
|
||||
if (cmd == "help") {
|
||||
addLine("Available commands:", COLOR_INFO);
|
||||
addLine(" clear - Clear console output", COLOR_RESULT);
|
||||
addLine(" help - Show this help", COLOR_RESULT);
|
||||
addLine(TR("console_available_commands"), COLOR_INFO);
|
||||
addLine(TR("console_help_clear"), COLOR_RESULT);
|
||||
addLine(TR("console_help_help"), COLOR_RESULT);
|
||||
addLine("", COLOR_RESULT);
|
||||
addLine("Common RPC commands (when connected):", COLOR_INFO);
|
||||
addLine(" getinfo - Get daemon info", COLOR_RESULT);
|
||||
addLine(" getbalance - Get transparent balance", COLOR_RESULT);
|
||||
addLine(" z_gettotalbalance - Get total balance", COLOR_RESULT);
|
||||
addLine(" getblockcount - Get current block height", COLOR_RESULT);
|
||||
addLine(" getpeerinfo - Get connected peers", COLOR_RESULT);
|
||||
addLine(" setgenerate true/false [threads] - Mining on/off", COLOR_RESULT);
|
||||
addLine(" getmininginfo - Get mining status", COLOR_RESULT);
|
||||
addLine(" stop - Stop the daemon", COLOR_RESULT);
|
||||
addLine(TR("console_common_rpc"), COLOR_INFO);
|
||||
addLine(TR("console_help_getinfo"), COLOR_RESULT);
|
||||
addLine(TR("console_help_getbalance"), COLOR_RESULT);
|
||||
addLine(TR("console_help_gettotalbalance"), COLOR_RESULT);
|
||||
addLine(TR("console_help_getblockcount"), COLOR_RESULT);
|
||||
addLine(TR("console_help_getpeerinfo"), COLOR_RESULT);
|
||||
addLine(TR("console_help_setgenerate"), COLOR_RESULT);
|
||||
addLine(TR("console_help_getmininginfo"), COLOR_RESULT);
|
||||
addLine(TR("console_help_stop"), COLOR_RESULT);
|
||||
addLine("", COLOR_RESULT);
|
||||
addLine("Click 'Commands' in the toolbar for full RPC reference", COLOR_INFO);
|
||||
addLine("Use Tab for command completion, Up/Down for history", COLOR_INFO);
|
||||
addLine(TR("console_click_commands"), COLOR_INFO);
|
||||
addLine(TR("console_tab_completion"), COLOR_INFO);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute RPC command
|
||||
if (!rpc || !rpc->isConnected()) {
|
||||
addLine("Error: Not connected to daemon", COLOR_ERROR);
|
||||
addLine(TR("console_not_connected"), COLOR_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1839,7 +1851,7 @@ void ConsoleTab::clear()
|
||||
last_xmrig_output_size_ = 0;
|
||||
}
|
||||
// addLine() takes the lock itself, so call it outside the locked scope
|
||||
addLine("Console cleared", COLOR_INFO);
|
||||
addLine(TR("console_cleared"), COLOR_INFO);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -119,6 +119,7 @@ private:
|
||||
size_t last_xmrig_output_size_ = 0;
|
||||
bool shown_startup_message_ = false;
|
||||
daemon::EmbeddedDaemon::State last_daemon_state_ = daemon::EmbeddedDaemon::State::Stopped;
|
||||
bool last_rpc_connected_ = false;
|
||||
|
||||
// Text selection state
|
||||
bool is_selecting_ = false;
|
||||
|
||||
@@ -69,16 +69,14 @@ void ExportAllKeysDialog::render(App* app)
|
||||
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
|
||||
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Export All Private Keys", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("export_keys_title"), &s_open, win.width, 0.94f)) {
|
||||
// Warning
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
ImGui::Text(ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextWrapped("DANGER: This will export ALL private keys from your wallet! "
|
||||
"Anyone with access to this file can steal your funds. "
|
||||
"Store it securely and delete after use.");
|
||||
ImGui::TextWrapped("%s", TR("export_keys_danger"));
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -90,19 +88,19 @@ void ExportAllKeysDialog::render(App* app)
|
||||
}
|
||||
|
||||
// Options
|
||||
ImGui::Text("Export options:");
|
||||
ImGui::Checkbox("Include Z-addresses (shielded)", &s_include_z);
|
||||
ImGui::Checkbox("Include T-addresses (transparent)", &s_include_t);
|
||||
ImGui::Text("%s", TR("export_keys_options"));
|
||||
ImGui::Checkbox(TR("export_keys_include_z"), &s_include_z);
|
||||
ImGui::Checkbox(TR("export_keys_include_t"), &s_include_t);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Filename
|
||||
ImGui::Text("Output filename:");
|
||||
ImGui::Text("%s", TR("output_filename"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("File will be saved in: ~/.config/ObsidianDragon/");
|
||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
||||
|
||||
if (s_exporting) {
|
||||
ImGui::EndDisabled();
|
||||
@@ -117,7 +115,7 @@ void ExportAllKeysDialog::render(App* app)
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton("Export Keys", ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||
if (material::StyledButton(TR("export_keys_btn"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||
if (!s_include_z && !s_include_t) {
|
||||
Notifications::instance().warning("Select at least one address type");
|
||||
} else if (!app->rpc() || !app->rpc()->isConnected()) {
|
||||
@@ -215,7 +213,7 @@ void ExportAllKeysDialog::render(App* app)
|
||||
s_exporting = false;
|
||||
if (writeOk) {
|
||||
s_status = "Exported to: " + filepath;
|
||||
Notifications::instance().success("Keys exported successfully");
|
||||
Notifications::instance().success(TR("export_keys_success"));
|
||||
} else {
|
||||
s_status = "Failed to write file";
|
||||
Notifications::instance().error("Failed to save key file");
|
||||
@@ -229,11 +227,11 @@ void ExportAllKeysDialog::render(App* app)
|
||||
if (s_exporting) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Exporting %d/%d...", s_exported_count, s_total_addresses);
|
||||
ImGui::TextDisabled(TR("export_keys_progress"), s_exported_count, s_total_addresses);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,31 +72,31 @@ void ExportTransactionsDialog::render(App* app)
|
||||
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
|
||||
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Export Transactions to CSV", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("export_tx_title"), &s_open, win.width, 0.94f)) {
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
ImGui::Text("Export %zu transactions to CSV file.", state.transactions.size());
|
||||
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Filename
|
||||
ImGui::Text("Output filename:");
|
||||
ImGui::Text("%s", TR("output_filename"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("File will be saved in: ~/.config/ObsidianDragon/");
|
||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Export button
|
||||
if (material::StyledButton("Export", ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||
if (state.transactions.empty()) {
|
||||
Notifications::instance().warning("No transactions to export");
|
||||
Notifications::instance().warning(TR("export_tx_none"));
|
||||
} else {
|
||||
std::string configDir = util::Platform::getConfigDir();
|
||||
std::string filepath = configDir + "/" + s_filename;
|
||||
@@ -104,7 +104,7 @@ void ExportTransactionsDialog::render(App* app)
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
s_status = "Failed to create file";
|
||||
Notifications::instance().error("Failed to create CSV file");
|
||||
Notifications::instance().error(TR("export_tx_file_fail"));
|
||||
} else {
|
||||
// Write CSV header
|
||||
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
|
||||
@@ -142,7 +142,7 @@ void ExportTransactionsDialog::render(App* app)
|
||||
|
||||
s_status = "Exported " + std::to_string(state.transactions.size()) +
|
||||
" transactions to: " + filepath;
|
||||
Notifications::instance().success("Transactions exported successfully");
|
||||
Notifications::instance().success(TR("export_tx_success"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,15 +113,14 @@ void ImportKeyDialog::render(App* app)
|
||||
auto importBtn = S.button("dialogs.import-key", "import-button");
|
||||
auto closeBtn = S.button("dialogs.import-key", "close-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Import Private Key", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("import_key_title"), &s_open, win.width, 0.94f)) {
|
||||
// Warning
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f));
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
ImGui::Text(ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextWrapped("Warning: Never share your private keys! "
|
||||
"Importing keys from untrusted sources can compromise your wallet.");
|
||||
ImGui::TextWrapped("%s", TR("import_key_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -129,13 +128,11 @@ void ImportKeyDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Key input
|
||||
ImGui::Text("Private Key(s):");
|
||||
ImGui::Text("%s", TR("import_key_label"));
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(?)");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Enter one or more private keys, one per line.\n"
|
||||
"Supports both z-address and t-address keys.\n"
|
||||
"Lines starting with # are treated as comments.");
|
||||
ImGui::SetTooltip("%s", TR("import_key_tooltip"));
|
||||
}
|
||||
|
||||
if (s_importing) {
|
||||
@@ -147,7 +144,7 @@ void ImportKeyDialog::render(App* app)
|
||||
ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput);
|
||||
|
||||
// Paste button
|
||||
if (material::StyledButton("Paste from Clipboard", ImVec2(0,0), S.resolveFont(importBtn.font))) {
|
||||
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);
|
||||
@@ -155,24 +152,24 @@ void ImportKeyDialog::render(App* app)
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Clear", ImVec2(0,0), S.resolveFont(importBtn.font))) {
|
||||
if (material::StyledButton(TR("clear"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
|
||||
s_key_input[0] = '\0';
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Rescan options
|
||||
ImGui::Checkbox("Rescan blockchain after import", &s_rescan);
|
||||
ImGui::Checkbox(TR("import_key_rescan"), &s_rescan);
|
||||
|
||||
if (s_rescan) {
|
||||
ImGui::Indent();
|
||||
ImGui::Text("Start height:");
|
||||
ImGui::Text("%s", TR("import_key_start_height"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(rescanInput.width);
|
||||
ImGui::InputInt("##RescanHeight", &s_rescan_height);
|
||||
if (s_rescan_height < 0) s_rescan_height = 0;
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(0 = full rescan)");
|
||||
ImGui::TextDisabled("%s", TR("import_key_full_rescan"));
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
@@ -189,13 +186,13 @@ void ImportKeyDialog::render(App* app)
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton("Import Key(s)", ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) {
|
||||
if (material::StyledButton(TR("import_key_btn"), ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) {
|
||||
auto keys = splitKeys(s_key_input);
|
||||
|
||||
if (keys.empty()) {
|
||||
Notifications::instance().warning("No valid keys found in input");
|
||||
Notifications::instance().warning(TR("import_key_no_valid"));
|
||||
} else if (!app->rpc() || !app->rpc()->isConnected()) {
|
||||
Notifications::instance().error("Not connected to daemon");
|
||||
Notifications::instance().error(TR("not_connected"));
|
||||
} else {
|
||||
s_importing = true;
|
||||
s_total_keys = static_cast<int>(keys.size());
|
||||
@@ -237,7 +234,7 @@ void ImportKeyDialog::render(App* app)
|
||||
imported, failed);
|
||||
s_status = buf;
|
||||
if (imported > 0) {
|
||||
Notifications::instance().success("Keys imported successfully");
|
||||
Notifications::instance().success(TR("import_key_success"));
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -248,11 +245,11 @@ void ImportKeyDialog::render(App* app)
|
||||
if (s_importing) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Importing %d/%d...", s_imported_keys + s_failed_keys, s_total_keys);
|
||||
ImGui::TextDisabled(TR("import_key_progress"), s_imported_keys + s_failed_keys, s_total_keys);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
@@ -271,9 +268,9 @@ void ImportKeyDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Help text
|
||||
ImGui::TextDisabled("Supported key formats:");
|
||||
ImGui::BulletText("Z-address spending keys (secret-extended-key-...)");
|
||||
ImGui::BulletText("T-address WIF private keys");
|
||||
ImGui::TextDisabled("%s", TR("import_key_formats"));
|
||||
ImGui::BulletText("%s", TR("import_key_z_format"));
|
||||
ImGui::BulletText("%s", TR("import_key_t_format"));
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +63,13 @@ void KeyExportDialog::render(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f));
|
||||
ImGui::BeginChild("WarningBox", ImVec2(-1, warningBox.height > 0 ? warningBox.height : 80), true);
|
||||
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " WARNING!");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " %s", TR("warning_upper"));
|
||||
ImGui::Spacing();
|
||||
|
||||
if (s_key_type == KeyType::Private) {
|
||||
ImGui::TextWrapped(" Keep this key SECRET! Anyone with this key can spend your "
|
||||
"funds. Never share it online or with untrusted parties.");
|
||||
ImGui::TextWrapped(" %s", TR("key_export_private_warning"));
|
||||
} else {
|
||||
ImGui::TextWrapped(" This viewing key allows others to see your incoming transactions "
|
||||
"and balance, but NOT spend your funds. Share only with trusted parties.");
|
||||
ImGui::TextWrapped(" %s", TR("key_export_viewing_warning"));
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
@@ -82,7 +80,7 @@ void KeyExportDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Address display
|
||||
ImGui::Text("Address:");
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
|
||||
// Determine if it's a z-address (longer) or t-address
|
||||
bool is_zaddr = s_address.length() > 50;
|
||||
@@ -105,16 +103,16 @@ void KeyExportDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Key display section
|
||||
const char* key_label = (s_key_type == KeyType::Private) ? "Private Key:" : "Viewing Key:";
|
||||
const char* key_label = (s_key_type == KeyType::Private) ? TR("key_export_private_key") : TR("key_export_viewing_key");
|
||||
ImGui::Text("%s", key_label);
|
||||
|
||||
if (s_fetching) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Fetching key from wallet...");
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching"));
|
||||
} else if (!s_error.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Error: %s", s_error.c_str());
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
|
||||
} else if (s_key.empty()) {
|
||||
// Show button to fetch key
|
||||
if (material::StyledButton("Reveal Key", ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||
if (material::StyledButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||
s_fetching = true;
|
||||
|
||||
// Check if z-address or t-address
|
||||
@@ -171,13 +169,13 @@ void KeyExportDialog::render(App* app)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
s_error = "Viewing keys are only available for shielded (z) addresses";
|
||||
s_error = TR("key_export_viewing_keys_zonly");
|
||||
s_fetching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Click to retrieve the key from your wallet");
|
||||
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
||||
} else {
|
||||
// Key has been fetched - display it
|
||||
|
||||
@@ -201,7 +199,7 @@ void KeyExportDialog::render(App* app)
|
||||
// Show/Hide and Copy buttons
|
||||
ImGui::Spacing();
|
||||
|
||||
if (material::StyledButton(s_show_key ? "Hide" : "Show", ImVec2(toggleBtn.width, 0), S.resolveFont(toggleBtn.font))) {
|
||||
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(toggleBtn.width, 0), S.resolveFont(toggleBtn.font))) {
|
||||
s_show_key = !s_show_key;
|
||||
}
|
||||
|
||||
@@ -222,7 +220,7 @@ void KeyExportDialog::render(App* app)
|
||||
float avail_width = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f);
|
||||
|
||||
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
// Clear sensitive data
|
||||
s_key.clear();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../data/exchange_info.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -147,15 +148,17 @@ void RenderMarketTab(App* app)
|
||||
float mkCardBudget = std::max(200.0f, marketAvail.y - mkOverhead);
|
||||
|
||||
Layout::SectionBudget mb(mkCardBudget);
|
||||
float statsMarketBudH = mb.allocate(0.14f, S.drawElement("tabs.market", "stats-card-min-height").size);
|
||||
float portfolioBudgetH = mb.allocate(0.18f, 50.0f);
|
||||
|
||||
// ================================================================
|
||||
// PRICE HERO — Large glass card with price + change badge
|
||||
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
|
||||
// ================================================================
|
||||
{
|
||||
float dp = Layout::dpiScale();
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float cardH = std::max(S.drawElement("tabs.market", "hero-card-min-height").size, S.drawElement("tabs.market", "hero-card-height").size * vs);
|
||||
float heroMinH = S.drawElement("tabs.market", "hero-card-min-height").size;
|
||||
float statsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad;
|
||||
float cardH = std::max(heroMinH + statsRowH + pad, (S.drawElement("tabs.market", "hero-card-height").size + statsRowH + pad) * vs);
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
@@ -172,36 +175,118 @@ void RenderMarketTab(App* app)
|
||||
float cy = cardMin.y + Layout::spacingLg();
|
||||
|
||||
if (market.price_usd > 0) {
|
||||
// Large price with text shadow
|
||||
// ---- HERO PRICE (large, prominent) ----
|
||||
ImFont* h3 = Type().h3();
|
||||
std::string priceStr = FormatPrice(market.price_usd);
|
||||
ImU32 priceCol = Success();
|
||||
DrawTextShadow(dl, h4, h4->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
|
||||
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
|
||||
|
||||
// BTC price beside it
|
||||
float priceW = h4->CalcTextSizeA(h4->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
|
||||
snprintf(buf, sizeof(buf), "%.10f BTC", market.price_btc);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx + priceW + Layout::spacingXl(), cy + (h4->LegacySize - capFont->LegacySize)),
|
||||
OnSurfaceMedium(), buf);
|
||||
// Ticker label after price
|
||||
float priceW = h3->CalcTextSizeA(h3->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
|
||||
dl->AddText(body2, body2->LegacySize,
|
||||
ImVec2(cx + priceW + Layout::spacingSm(), cy + (h3->LegacySize - body2->LegacySize)),
|
||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||
|
||||
// 24h change badge
|
||||
float badgeY = cy + h4->LegacySize + 8;
|
||||
// 24h change badge — to the right of ticker
|
||||
float tickerW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, DRAGONX_TICKER).x;
|
||||
float badgeX = cx + priceW + Layout::spacingSm() + tickerW + Layout::spacingMd();
|
||||
ImU32 chgCol = market.change_24h >= 0 ? Success() : Error();
|
||||
snprintf(buf, sizeof(buf), "%s%.2f%%", market.change_24h >= 0 ? "+" : "", market.change_24h);
|
||||
|
||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float badgePad = Layout::spacingSm() + Layout::spacingXs();
|
||||
ImVec2 bMin(cx, badgeY);
|
||||
ImVec2 bMax(cx + txtSz.x + badgePad * 2, badgeY + txtSz.y + badgePad);
|
||||
snprintf(buf, sizeof(buf), "%s%.2f%% 24h", market.change_24h >= 0 ? "+" : "", market.change_24h);
|
||||
ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float badgePadH = Layout::spacingSm();
|
||||
float badgePadV = Layout::spacingXs();
|
||||
ImVec2 bMin(badgeX, cy + (h3->LegacySize - chgSz.y - badgePadV * 2) * 0.5f);
|
||||
ImVec2 bMax(badgeX + chgSz.x + badgePadH * 2, bMin.y + chgSz.y + badgePadV * 2);
|
||||
ImU32 badgeBg = market.change_24h >= 0 ? WithAlpha(Success(), 30) : WithAlpha(Error(), 30);
|
||||
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + badgePad, badgeY + badgePad * 0.5f), chgCol, buf);
|
||||
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
|
||||
|
||||
// "24h" label after badge
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(bMax.x + 6, badgeY + badgePad * 0.5f), OnSurfaceDisabled(), "24h");
|
||||
// ---- SEPARATOR ----
|
||||
float sepY = cy + h3->LegacySize + Layout::spacingMd();
|
||||
float rnd = glassSpec.rounding;
|
||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, sepY),
|
||||
ImVec2(cardMax.x - rnd * 0.5f, sepY),
|
||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||
|
||||
// ---- STATS ROW (BTC Price | Volume | Market Cap) ----
|
||||
float statsY = sepY + Layout::spacingSm();
|
||||
float colW = (availWidth - Layout::spacingLg() * 2) / 3.0f;
|
||||
|
||||
struct StatItem { const char* label; std::string value; ImU32 valueCol; };
|
||||
StatItem stats[3] = {
|
||||
{TR("market_btc_price"), "", OnSurface()},
|
||||
{TR("market_24h_volume"), FormatCompactUSD(market.volume_24h), OnSurface()},
|
||||
{TR("market_cap"), FormatCompactUSD(market.market_cap), OnSurface()},
|
||||
};
|
||||
snprintf(buf, sizeof(buf), "%.10f", market.price_btc);
|
||||
stats[0].value = buf;
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float sx = cardMin.x + Layout::spacingLg() + i * colW;
|
||||
float centerX = sx + colW * 0.5f;
|
||||
|
||||
// Label (overline, centered)
|
||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[i].label);
|
||||
dl->AddText(ovFont, ovFont->LegacySize,
|
||||
ImVec2(centerX - lblSz.x * 0.5f, statsY), OnSurfaceMedium(), stats[i].label);
|
||||
|
||||
// Value (subtitle1, centered)
|
||||
float valY = statsY + ovFont->LegacySize + Layout::spacingXs();
|
||||
ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[i].value.c_str());
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
|
||||
}
|
||||
|
||||
// ---- TRADE BUTTON (top-right of card) ----
|
||||
if (!currentExchange.pairs.empty()) {
|
||||
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, pairName);
|
||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_OPEN_IN_NEW);
|
||||
float iconGap = Layout::spacingSm();
|
||||
float tradePadH = Layout::spacingMd();
|
||||
float tradePadV = Layout::spacingSm();
|
||||
float tradeBtnW = textSz.x + iconGap + iconSz.x + tradePadH * 2;
|
||||
float tradeBtnH = std::max(textSz.y, iconSz.y) + tradePadV * 2;
|
||||
float tradeBtnX = cardMax.x - pad - tradeBtnW;
|
||||
float tradeBtnY = cardMin.y + Layout::spacingSm();
|
||||
|
||||
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
|
||||
bool tradeHov = material::IsRectHovered(tMin, tMax);
|
||||
|
||||
// Glass pill background
|
||||
GlassPanelSpec tradeBtnGlass;
|
||||
tradeBtnGlass.rounding = tradeBtnH * 0.5f;
|
||||
tradeBtnGlass.fillAlpha = tradeHov ? 35 : 20;
|
||||
DrawGlassPanel(dl, tMin, tMax, tradeBtnGlass);
|
||||
if (tradeHov)
|
||||
dl->AddRectFilled(tMin, tMax, WithAlpha(Primary(), 20), tradeBtnH * 0.5f);
|
||||
|
||||
// Text (pair name with body2, icon with icon font)
|
||||
ImU32 tradeCol = tradeHov ? OnSurface() : OnSurfaceMedium();
|
||||
float contentY = tradeBtnY + tradePadV;
|
||||
float curX = tradeBtnX + tradePadH;
|
||||
dl->AddText(body2, body2->LegacySize,
|
||||
ImVec2(curX, contentY), tradeCol, pairName);
|
||||
curX += textSz.x + iconGap;
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(curX, contentY + (textSz.y - iconSz.y) * 0.5f), tradeCol, ICON_MD_OPEN_IN_NEW);
|
||||
|
||||
// Click
|
||||
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos(tMin);
|
||||
ImGui::InvisibleButton("##TradeOnExchange", ImVec2(tradeBtnW, tradeBtnH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(TR("market_trade_on"), currentExchange.name.c_str());
|
||||
}
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
}
|
||||
} else {
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), "Price data unavailable");
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), TR("market_price_unavailable"));
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
@@ -209,42 +294,6 @@ void RenderMarketTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// STATS — Three glass cards (Price | Volume | Market Cap)
|
||||
// ================================================================
|
||||
{
|
||||
float cardW = (availWidth - 2 * gap) / 3.0f;
|
||||
float cardH = std::min(StatCardHeight(vs), statsMarketBudH);
|
||||
ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
|
||||
struct StatInfo { const char* label; std::string value; ImU32 col; ImU32 accent; };
|
||||
StatInfo cards[3] = {
|
||||
{"PRICE", market.price_usd > 0 ? FormatPrice(market.price_usd) : "N/A",
|
||||
OnSurface(), WithAlpha(Success(), 200)},
|
||||
{"24H VOLUME", FormatCompactUSD(market.volume_24h),
|
||||
OnSurface(), WithAlpha(Secondary(), 200)},
|
||||
{"MARKET CAP", FormatCompactUSD(market.market_cap),
|
||||
OnSurface(), WithAlpha(Warning(), 200)},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float xOff = i * (cardW + gap);
|
||||
ImVec2 cMin(origin.x + xOff, origin.y);
|
||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
||||
|
||||
StatCardSpec sc;
|
||||
sc.overline = cards[i].label;
|
||||
sc.value = cards[i].value.c_str();
|
||||
sc.valueCol = cards[i].col;
|
||||
sc.accentCol = cards[i].accent;
|
||||
sc.centered = true;
|
||||
DrawStatCard(dl, cMin, cMax, sc, glassSpec);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(availWidth, cardH));
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PRICE CHART — Custom drawn inside glass panel (matches app design)
|
||||
// ================================================================
|
||||
@@ -340,7 +389,7 @@ void RenderMarketTab(App* app)
|
||||
|
||||
// X-axis labels
|
||||
const int xlabels[] = {0, 6, 12, 18, 23};
|
||||
const char* xlabelText[] = {"24h", "18h", "12h", "6h", "Now"};
|
||||
const char* xlabelText[] = {TR("market_24h"), TR("market_18h"), TR("market_12h"), TR("market_6h"), TR("market_now")};
|
||||
for (int xi = 0; xi < 5; xi++) {
|
||||
int idx = xlabels[xi];
|
||||
float t = (float)idx / (float)(n - 1);
|
||||
@@ -395,7 +444,7 @@ void RenderMarketTab(App* app)
|
||||
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
||||
}
|
||||
} else {
|
||||
const char* msg = "No price history available";
|
||||
const char* msg = TR("market_no_history");
|
||||
ImVec2 ts = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, msg);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, chartMin.y + chartH * 0.45f),
|
||||
@@ -429,7 +478,7 @@ void RenderMarketTab(App* app)
|
||||
s_history_initialized = false;
|
||||
s_last_refresh_time = ImGui::GetTime();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh price data");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("market_refresh_price"));
|
||||
|
||||
// Timestamp text to the left of refresh button
|
||||
if (s_last_refresh_time > 0.0) {
|
||||
@@ -452,7 +501,7 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// EXCHANGE SELECTOR — Combo dropdown + pair name + trade link
|
||||
// EXCHANGE SELECTOR — Combo dropdown + attribution
|
||||
// ================================================================
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.market", "exchange-top-gap").size));
|
||||
{
|
||||
@@ -483,28 +532,9 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// Show current pair name beside combo
|
||||
if (!currentExchange.pairs.empty()) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
Type().textColored(TypeStyle::Subtitle1, OnSurface(),
|
||||
currentExchange.pairs[s_pair_idx].displayName.c_str());
|
||||
|
||||
// "Open on exchange" button
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::PushFont(material::Typography::instance().iconSmall());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4));
|
||||
snprintf(buf, sizeof(buf), ICON_MD_OPEN_IN_NEW "##TradeLink");
|
||||
if (ImGui::Button(buf)) {
|
||||
util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl);
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open on %s", currentExchange.name.c_str());
|
||||
}
|
||||
|
||||
// Attribution
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "Price data from CoinGecko API");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("market_attribution"));
|
||||
|
||||
if (!market.last_updated.empty()) {
|
||||
ImGui::SameLine(0, 12);
|
||||
@@ -722,7 +752,7 @@ void RenderMarketTab(App* app)
|
||||
// PORTFOLIO — Glass card with balance breakdown
|
||||
// ================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PORTFOLIO");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
double total_balance = state.totalBalance;
|
||||
@@ -760,7 +790,7 @@ void RenderMarketTab(App* app)
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cardMax.x - valW - pad, cy + 2), OnSurfaceMedium(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), "No price data");
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), TR("market_no_price"));
|
||||
}
|
||||
|
||||
cy += sub1->LegacySize + 8;
|
||||
@@ -799,7 +829,7 @@ void RenderMarketTab(App* app)
|
||||
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f);
|
||||
|
||||
int pct = (int)(shieldedRatio * 100.0f + 0.5f);
|
||||
snprintf(buf, sizeof(buf), "%d%% Shielded", pct);
|
||||
snprintf(buf, sizeof(buf), TR("market_pct_shielded"), pct);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx, cy + barH + 2), OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "mining_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../config/settings.h"
|
||||
@@ -38,6 +39,9 @@ static bool s_threads_initialized = false;
|
||||
static bool s_drag_active = false;
|
||||
static int s_drag_anchor_thread = 0; // thread# where drag started
|
||||
|
||||
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
|
||||
static int s_earnings_filter = 0;
|
||||
|
||||
// Pool mode state
|
||||
static bool s_pool_mode = false;
|
||||
static char s_pool_url[256] = "pool.dragonx.is:3433";
|
||||
@@ -126,7 +130,13 @@ void RenderMiningTab(App* app)
|
||||
int max_threads = GetMaxMiningThreads();
|
||||
|
||||
if (!s_threads_initialized) {
|
||||
s_selected_threads = mining.generate ? std::max(1, mining.genproclimit) : 1;
|
||||
int saved = app->settings()->getPoolThreads();
|
||||
if (mining.generate)
|
||||
s_selected_threads = std::max(1, mining.genproclimit);
|
||||
else if (saved > 0)
|
||||
s_selected_threads = std::min(saved, max_threads);
|
||||
else
|
||||
s_selected_threads = 1;
|
||||
s_threads_initialized = true;
|
||||
}
|
||||
|
||||
@@ -234,7 +244,7 @@ void RenderMiningTab(App* app)
|
||||
dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd);
|
||||
}
|
||||
{
|
||||
const char* label = "SOLO";
|
||||
const char* label = TR("mining_solo");
|
||||
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||
float lx = soloMin.x + (toggleW - sz.x) * 0.5f;
|
||||
float ly = soloMin.y + (toggleH - sz.y) * 0.5f;
|
||||
@@ -255,7 +265,7 @@ void RenderMiningTab(App* app)
|
||||
dl->AddRectFilled(poolMin, poolMax, WithAlpha(OnSurface(), 20), toggleRnd);
|
||||
}
|
||||
{
|
||||
const char* label = "POOL";
|
||||
const char* label = TR("mining_pool");
|
||||
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||
float lx = poolMin.x + (toggleW - sz.x) * 0.5f;
|
||||
float ly = poolMin.y + (toggleH - sz.y) * 0.5f;
|
||||
@@ -287,7 +297,7 @@ void RenderMiningTab(App* app)
|
||||
}
|
||||
if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (poolHov && soloMiningActive && !s_pool_mode) {
|
||||
ImGui::SetTooltip("Stop solo mining to use pool mining");
|
||||
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||
@@ -307,7 +317,7 @@ void RenderMiningTab(App* app)
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::PushFont(capFont);
|
||||
ImGui::TextUnformatted("Stop solo mining to configure pool settings");
|
||||
ImGui::TextUnformatted(TR("mining_stop_solo_for_pool_settings"));
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleColor();
|
||||
} else if (s_pool_mode) {
|
||||
@@ -339,7 +349,7 @@ void RenderMiningTab(App* app)
|
||||
|
||||
// === Pool URL input ===
|
||||
ImGui::SetNextItemWidth(urlW);
|
||||
if (ImGui::InputTextWithHint("##PoolURL", "Pool URL", s_pool_url, sizeof(s_pool_url))) {
|
||||
if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) {
|
||||
s_pool_settings_dirty = true;
|
||||
}
|
||||
|
||||
@@ -357,7 +367,7 @@ void RenderMiningTab(App* app)
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Saved pools");
|
||||
ImGui::SetTooltip("%s", TR("mining_saved_pools"));
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||
@@ -389,7 +399,7 @@ void RenderMiningTab(App* app)
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save pool URL");
|
||||
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||
@@ -416,11 +426,19 @@ void RenderMiningTab(App* app)
|
||||
if (savedUrls.empty()) {
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("No saved pools");
|
||||
ImGui::TextDisabled("%s", TR("mining_no_saved_pools"));
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
|
||||
ImGui::TextDisabled("%s", TR("mining_click"));
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 2 * dp);
|
||||
ImGui::PushFont(Type().iconSmall());
|
||||
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 2 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("%s", TR("mining_to_save"));
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
std::string urlToRemove;
|
||||
@@ -457,7 +475,7 @@ void RenderMiningTab(App* app)
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Remove");
|
||||
ImGui::SetTooltip("%s", TR("mining_remove"));
|
||||
} else if (rowHov) {
|
||||
// Show faint X when row is hovered
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
@@ -508,11 +526,11 @@ void RenderMiningTab(App* app)
|
||||
float wrkGroupW = wrkW + perGroupExtra;
|
||||
|
||||
ImGui::SetNextItemWidth(wrkW);
|
||||
if (ImGui::InputTextWithHint("##PoolWorker", "Payout Address", s_pool_worker, sizeof(s_pool_worker))) {
|
||||
if (ImGui::InputTextWithHint("##PoolWorker", TR("mining_payout_address"), s_pool_worker, sizeof(s_pool_worker))) {
|
||||
s_pool_settings_dirty = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Your DRAGONX address for receiving pool payouts");
|
||||
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
|
||||
}
|
||||
|
||||
// --- Worker: Dropdown arrow button ---
|
||||
@@ -529,7 +547,7 @@ void RenderMiningTab(App* app)
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Saved addresses");
|
||||
ImGui::SetTooltip("%s", TR("mining_saved_addresses"));
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||
@@ -561,7 +579,7 @@ void RenderMiningTab(App* app)
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save payout address");
|
||||
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||
@@ -589,11 +607,19 @@ void RenderMiningTab(App* app)
|
||||
if (savedWorkers.empty()) {
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("No saved addresses");
|
||||
ImGui::TextDisabled("%s", TR("mining_no_saved_addresses"));
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
|
||||
ImGui::TextDisabled("%s", TR("mining_click"));
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 2 * dp);
|
||||
ImGui::PushFont(Type().iconSmall());
|
||||
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 2 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("%s", TR("mining_to_save"));
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
std::string addrToRemove;
|
||||
@@ -633,7 +659,7 @@ void RenderMiningTab(App* app)
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Remove");
|
||||
ImGui::SetTooltip("%s", TR("mining_remove"));
|
||||
} else if (rowHov) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
@@ -694,7 +720,7 @@ void RenderMiningTab(App* app)
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Reset to defaults");
|
||||
ImGui::SetTooltip("%s", TR("mining_reset_defaults"));
|
||||
}
|
||||
|
||||
// Icon
|
||||
@@ -781,9 +807,9 @@ void RenderMiningTab(App* app)
|
||||
// --- Header row: "THREADS 4 / 16" + RAM est + active indicator ---
|
||||
{
|
||||
ImVec2 labelPos(cardMin.x + pad, curY);
|
||||
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), "THREADS");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), TR("mining_threads"));
|
||||
|
||||
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, "THREADS").x;
|
||||
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, TR("mining_threads")).x;
|
||||
snprintf(buf, sizeof(buf), " %d / %d", s_selected_threads, max_threads);
|
||||
ImVec2 countPos(labelPos.x + labelW, curY);
|
||||
dl->AddText(sub1, sub1->LegacySize, countPos, OnSurface(), buf);
|
||||
@@ -804,17 +830,58 @@ void RenderMiningTab(App* app)
|
||||
OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// Active mining indicator (top-right)
|
||||
// Idle mining toggle (top-right corner of card)
|
||||
float idleRightEdge = cardMax.x - pad;
|
||||
{
|
||||
bool idleOn = app->settings()->getMineWhenIdle();
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* idleIcon = ICON_MD_SCHEDULE;
|
||||
float icoH = icoFont->LegacySize;
|
||||
float btnSz = icoH + 8.0f * dp;
|
||||
float btnX = idleRightEdge - btnSz;
|
||||
float btnY = curY + (headerH - btnSz) * 0.5f;
|
||||
|
||||
// Pill background when active
|
||||
if (idleOn) {
|
||||
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
||||
WithAlpha(Primary(), 60), btnSz * 0.5f);
|
||||
}
|
||||
|
||||
// Icon centered in button
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
|
||||
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
|
||||
dl->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
|
||||
icoCol, idleIcon);
|
||||
|
||||
// Click target (save/restore cursor so layout is unaffected)
|
||||
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
app->settings()->setMineWhenIdle(!idleOn);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
|
||||
}
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
|
||||
idleRightEdge = btnX - 4.0f * dp;
|
||||
}
|
||||
|
||||
// Active mining indicator (left of idle toggle)
|
||||
if (mining.generate) {
|
||||
float pulse = effects::isLowSpecMode()
|
||||
? schema::UI().drawElement("animations", "pulse-base-normal").size
|
||||
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
|
||||
ImU32 pulseCol = WithAlpha(Success(), (int)(255 * pulse));
|
||||
float dotR = schema::UI().drawElement("tabs.mining", "active-dot-radius").size + 2.0f * hs;
|
||||
dl->AddCircleFilled(ImVec2(cardMax.x - pad - dotR * 2, curY + dotR + 1 * dp), dotR, pulseCol);
|
||||
dl->AddCircleFilled(ImVec2(idleRightEdge - dotR, curY + dotR + 1 * dp), dotR, pulseCol);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cardMax.x - pad - dotR * 2 - 60 * hs, curY),
|
||||
WithAlpha(Success(), 200), "Mining");
|
||||
ImVec2(idleRightEdge - dotR - dotR - 60 * hs, curY),
|
||||
WithAlpha(Success(), 200), TR("mining_active"));
|
||||
}
|
||||
curY += headerH + secGap;
|
||||
}
|
||||
@@ -930,12 +997,16 @@ void RenderMiningTab(App* app)
|
||||
dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf);
|
||||
}
|
||||
|
||||
if (threads_changed && mining.generate) {
|
||||
app->startMining(s_selected_threads);
|
||||
}
|
||||
if (threads_changed && s_pool_mode && state.pool_mining.xmrig_running) {
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(s_selected_threads);
|
||||
if (threads_changed) {
|
||||
app->settings()->setPoolThreads(s_selected_threads);
|
||||
app->settings()->save();
|
||||
if (mining.generate) {
|
||||
app->startMining(s_selected_threads);
|
||||
}
|
||||
if (s_pool_mode && state.pool_mining.xmrig_running) {
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(s_selected_threads);
|
||||
}
|
||||
}
|
||||
|
||||
curY += gridH + secGap;
|
||||
@@ -1059,20 +1130,20 @@ void RenderMiningTab(App* app)
|
||||
const char* label;
|
||||
ImU32 lblCol;
|
||||
if (isToggling) {
|
||||
label = isMiningActive ? "STOPPING" : "STARTING";
|
||||
label = isMiningActive ? TR("mining_stopping") : TR("mining_starting");
|
||||
// Animated dots effect via alpha pulse
|
||||
float pulse = effects::isLowSpecMode()
|
||||
? 0.7f
|
||||
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
|
||||
lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
|
||||
} else if (isMiningActive) {
|
||||
label = "STOP";
|
||||
label = TR("mining_stop");
|
||||
lblCol = WithAlpha(Error(), 220);
|
||||
} else if (disabled) {
|
||||
label = "MINE";
|
||||
label = TR("mining_mine");
|
||||
lblCol = WithAlpha(OnSurface(), 50);
|
||||
} else {
|
||||
label = "MINE";
|
||||
label = TR("mining_mine");
|
||||
lblCol = WithAlpha(OnSurface(), 160);
|
||||
}
|
||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||
@@ -1086,13 +1157,13 @@ void RenderMiningTab(App* app)
|
||||
if (!disabled)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (isToggling)
|
||||
ImGui::SetTooltip(isMiningActive ? "Stopping miner..." : "Starting miner...");
|
||||
ImGui::SetTooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
||||
else if (isSyncing && !s_pool_mode)
|
||||
ImGui::SetTooltip("Syncing blockchain... (%.1f%%)", state.sync.verification_progress * 100.0);
|
||||
ImGui::SetTooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
||||
else if (poolBlockedBySolo)
|
||||
ImGui::SetTooltip("Stop solo mining before starting pool mining");
|
||||
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||
else
|
||||
ImGui::SetTooltip(isMiningActive ? "Stop Mining" : "Start Mining");
|
||||
ImGui::SetTooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
|
||||
}
|
||||
|
||||
// Click action — pool or solo
|
||||
@@ -1201,23 +1272,15 @@ void RenderMiningTab(App* app)
|
||||
int numStats = 3;
|
||||
|
||||
if (s_pool_mode) {
|
||||
col1Label = "POOL HASHRATE";
|
||||
col1Label = TR("mining_local_hashrate");
|
||||
col1Str = FormatHashrate(state.pool_mining.hashrate_10s);
|
||||
col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled();
|
||||
|
||||
col2Label = "THREADS / MEM";
|
||||
{
|
||||
char buf[64];
|
||||
int64_t memMB = state.pool_mining.memory_used / (1024 * 1024);
|
||||
if (memMB > 0)
|
||||
snprintf(buf, sizeof(buf), "%d / %lld MB", state.pool_mining.threads_active, (long long)memMB);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%d / --", state.pool_mining.threads_active);
|
||||
col2Str = buf;
|
||||
}
|
||||
col2Col = OnSurface();
|
||||
col2Label = TR("mining_pool_hashrate");
|
||||
col2Str = FormatHashrate(state.pool_mining.pool_hashrate);
|
||||
col2Col = state.pool_mining.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled();
|
||||
|
||||
col3Label = "SHARES";
|
||||
col3Label = TR("mining_shares");
|
||||
char sharesBuf[64];
|
||||
snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld",
|
||||
(long long)state.pool_mining.accepted,
|
||||
@@ -1225,7 +1288,7 @@ void RenderMiningTab(App* app)
|
||||
col3Str = sharesBuf;
|
||||
col3Col = OnSurface();
|
||||
|
||||
col4Label = "UPTIME";
|
||||
col4Label = TR("mining_uptime");
|
||||
int64_t up = state.pool_mining.uptime_sec;
|
||||
char uptBuf[64];
|
||||
if (up <= 0)
|
||||
@@ -1240,15 +1303,15 @@ void RenderMiningTab(App* app)
|
||||
} else {
|
||||
double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty);
|
||||
|
||||
col1Label = "LOCAL HASHRATE";
|
||||
col1Label = TR("mining_local_hashrate");
|
||||
col1Str = FormatHashrate(mining.localHashrate);
|
||||
col1Col = mining.generate ? greenCol : OnSurfaceDisabled();
|
||||
|
||||
col2Label = "NETWORK";
|
||||
col2Label = TR("mining_network");
|
||||
col2Str = FormatHashrate(mining.networkHashrate);
|
||||
col2Col = OnSurface();
|
||||
|
||||
col3Label = "EST. BLOCK";
|
||||
col3Label = TR("mining_est_block");
|
||||
col3Str = FormatEstTime(est_hours);
|
||||
col3Col = OnSurface();
|
||||
}
|
||||
@@ -1402,9 +1465,9 @@ void RenderMiningTab(App* app)
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(plotLeft, chartBot - capFont->LegacySize - 2 * dp),
|
||||
OnSurfaceDisabled(),
|
||||
chartHistory.size() >= 300 ? "5m ago" :
|
||||
chartHistory.size() >= 60 ? "1m ago" : "start");
|
||||
std::string nowLbl = "now";
|
||||
chartHistory.size() >= 300 ? TR("mining_chart_5m_ago") :
|
||||
chartHistory.size() >= 60 ? TR("mining_chart_1m_ago") : TR("mining_chart_start"));
|
||||
std::string nowLbl = TR("mining_chart_now");
|
||||
ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(plotRight - nowSz.x, chartBot - capFont->LegacySize - 2 * dp),
|
||||
@@ -1421,7 +1484,7 @@ void RenderMiningTab(App* app)
|
||||
if (hasLogContent || hasChartContent) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
|
||||
const char* toggleTip = showLogFlag ? "Show hashrate chart" : "Show mining log";
|
||||
const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log");
|
||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
|
||||
float btnSize = iconSz.y + 8 * dp;
|
||||
float btnX = cardMax.x - pad - btnSize;
|
||||
@@ -1486,6 +1549,10 @@ void RenderMiningTab(App* app)
|
||||
&& !tx.memo.empty()
|
||||
&& tx.memo.find("Mining Pool payout") != std::string::npos);
|
||||
if (isSoloMined || isPoolPayout) {
|
||||
// Apply earnings filter
|
||||
if (s_earnings_filter == 1 && !isSoloMined) continue;
|
||||
if (s_earnings_filter == 2 && !isPoolPayout) continue;
|
||||
|
||||
double amt = std::abs(tx.amount);
|
||||
minedAllTime += amt;
|
||||
minedAllTimeCount++;
|
||||
@@ -1526,6 +1593,63 @@ void RenderMiningTab(App* app)
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + combinedCardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// === Earnings filter toggle button (top-right of card) ===
|
||||
{
|
||||
const char* filterLabels[] = { TR("mining_filter_all"), TR("mining_solo"), TR("mining_pool") };
|
||||
const char* filterIcons[] = { ICON_MD_FUNCTIONS, ICON_MD_MEMORY, ICON_MD_CLOUD };
|
||||
const char* curIcon = filterIcons[s_earnings_filter];
|
||||
const char* curLabel = filterLabels[s_earnings_filter];
|
||||
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
float icoH = icoFont->LegacySize;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, curIcon);
|
||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, curLabel);
|
||||
float padH = Layout::spacingSm();
|
||||
float btnW = padH + icoSz.x + Layout::spacingXs() + lblSz.x + padH;
|
||||
float btnH = icoH + 8.0f * dp;
|
||||
float btnX = cardMax.x - pad - btnW;
|
||||
float btnY = cardMin.y + (earningsRowH - btnH) * 0.5f;
|
||||
|
||||
ImVec2 bMin(btnX, btnY), bMax(btnX + btnW, btnY + btnH);
|
||||
bool hov = material::IsRectHovered(bMin, bMax);
|
||||
|
||||
// Pill background
|
||||
ImU32 pillBg = s_earnings_filter != 0
|
||||
? WithAlpha(Primary(), 60)
|
||||
: WithAlpha(OnSurface(), hov ? 25 : 12);
|
||||
dl->AddRectFilled(bMin, bMax, pillBg, btnH * 0.5f);
|
||||
|
||||
// Icon
|
||||
ImU32 icoCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceDisabled());
|
||||
float cx = bMin.x + padH;
|
||||
float cy = bMin.y + (btnH - icoSz.y) * 0.5f;
|
||||
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(cx, cy), icoCol, curIcon);
|
||||
|
||||
// Label
|
||||
ImU32 lblCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceMedium());
|
||||
float lx = cx + icoSz.x + Layout::spacingXs();
|
||||
float ly = bMin.y + (btnH - lblSz.y) * 0.5f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), lblCol, curLabel);
|
||||
|
||||
// Click target
|
||||
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos(bMin);
|
||||
ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
s_earnings_filter = (s_earnings_filter + 1) % 3;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const char* tips[] = {
|
||||
TR("mining_filter_tip_all"),
|
||||
TR("mining_filter_tip_solo"),
|
||||
TR("mining_filter_tip_pool")
|
||||
};
|
||||
ImGui::SetTooltip("%s", tips[s_earnings_filter]);
|
||||
}
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
}
|
||||
|
||||
// === Earnings section (top of combined card) ===
|
||||
{
|
||||
const int numCols = 4;
|
||||
@@ -1561,10 +1685,10 @@ void RenderMiningTab(App* app)
|
||||
snprintf(estVal, sizeof(estVal), "N/A");
|
||||
|
||||
EarningsEntry entries[] = {
|
||||
{ "TODAY", todayVal, todaySub, greenCol2 },
|
||||
{ "YESTERDAY", yesterdayVal, yesterdaySub, OnSurface() },
|
||||
{ "ALL TIME", allVal, allSub, OnSurface() },
|
||||
{ "EST. DAILY", estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() },
|
||||
{ TR("mining_today"), todayVal, todaySub, greenCol2 },
|
||||
{ TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() },
|
||||
{ TR("mining_all_time"), allVal, allSub, OnSurface() },
|
||||
{ TR("mining_est_daily"), estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() },
|
||||
};
|
||||
|
||||
for (int ei = 0; ei < numCols; ei++) {
|
||||
@@ -1619,7 +1743,7 @@ void RenderMiningTab(App* app)
|
||||
float col3X = cx + colW * 2.0f;
|
||||
|
||||
// -- Difficulty --
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X, cy), OnSurfaceMedium(), "Difficulty");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X, cy), OnSurfaceMedium(), TR("difficulty"));
|
||||
if (mining.difficulty > 0) {
|
||||
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
|
||||
@@ -1628,19 +1752,19 @@ void RenderMiningTab(App* app)
|
||||
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Click to copy difficulty");
|
||||
ImGui::SetTooltip("%s", TR("mining_click_copy_difficulty"));
|
||||
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(buf);
|
||||
Notifications::instance().info("Difficulty copied");
|
||||
Notifications::instance().info(TR("mining_difficulty_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
// -- Block --
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), "Block");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), TR("block"));
|
||||
if (mining.blocks > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", mining.blocks);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
|
||||
@@ -1649,19 +1773,19 @@ void RenderMiningTab(App* app)
|
||||
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Click to copy block height");
|
||||
ImGui::SetTooltip("%s", TR("mining_click_copy_block"));
|
||||
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(buf);
|
||||
Notifications::instance().info("Block height copied");
|
||||
Notifications::instance().info(TR("mining_block_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
// -- Mining Address --
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), "Mining Addr");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr"));
|
||||
std::string mining_address = "";
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||
@@ -1685,14 +1809,14 @@ void RenderMiningTab(App* app)
|
||||
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Click to copy mining address");
|
||||
ImGui::SetTooltip("%s", TR("mining_click_copy_address"));
|
||||
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(mining_address.c_str());
|
||||
Notifications::instance().info("Mining address copied");
|
||||
Notifications::instance().info(TR("mining_address_copied"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1859,15 +1983,15 @@ void RenderMiningTab(App* app)
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
if (selfRAM >= 1024.0)
|
||||
ImGui::Text("Wallet: %.1f GB", selfRAM / 1024.0);
|
||||
ImGui::Text(TR("ram_wallet_gb"), selfRAM / 1024.0);
|
||||
else
|
||||
ImGui::Text("Wallet: %.0f MB", selfRAM);
|
||||
ImGui::Text(TR("ram_wallet_mb"), selfRAM);
|
||||
if (daemonRAM >= 1024.0)
|
||||
ImGui::Text("Daemon: %.1f GB (%s)", daemonRAM / 1024.0, app->getDaemonMemDiag().c_str());
|
||||
ImGui::Text(TR("ram_daemon_gb"), daemonRAM / 1024.0, app->getDaemonMemDiag().c_str());
|
||||
else
|
||||
ImGui::Text("Daemon: %.0f MB (%s)", daemonRAM, app->getDaemonMemDiag().c_str());
|
||||
ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("System: %.1f / %.0f GB", usedRAM / 1024.0, totalRAM / 1024.0);
|
||||
ImGui::Text(TR("ram_system_gb"), usedRAM / 1024.0, totalRAM / 1024.0);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
@@ -1881,7 +2005,7 @@ void RenderMiningTab(App* app)
|
||||
// ============================================================
|
||||
if (!recentMined.empty() || s_pool_mode) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
|
||||
s_pool_mode ? "RECENT POOL PAYOUTS" : "RECENT BLOCKS");
|
||||
s_pool_mode ? TR("mining_recent_payouts") : TR("mining_recent_blocks"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
|
||||
@@ -1924,8 +2048,8 @@ void RenderMiningTab(App* app)
|
||||
ImVec2(centerX - iSz.x * 0.5f, emptyY),
|
||||
OnSurfaceDisabled(), emptyIcon);
|
||||
const char* emptyMsg = s_pool_mode
|
||||
? "No pool payouts yet"
|
||||
: "No blocks found yet";
|
||||
? TR("mining_no_payouts_yet")
|
||||
: TR("mining_no_blocks_yet");
|
||||
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg);
|
||||
miningChildDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()),
|
||||
@@ -1966,13 +2090,13 @@ void RenderMiningTab(App* app)
|
||||
// Time
|
||||
int64_t diff = now - mtx.timestamp;
|
||||
if (diff < 60)
|
||||
snprintf(buf, sizeof(buf), "%llds ago", (long long)diff);
|
||||
snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff);
|
||||
else if (diff < 3600)
|
||||
snprintf(buf, sizeof(buf), "%lldm ago", (long long)(diff / 60));
|
||||
snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60));
|
||||
else if (diff < 86400)
|
||||
snprintf(buf, sizeof(buf), "%lldh ago", (long long)(diff / 3600));
|
||||
snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600));
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%lldd ago", (long long)(diff / 86400));
|
||||
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400));
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + iSz.x + 8 * dp, ry), OnSurfaceDisabled(), buf);
|
||||
|
||||
// Amount
|
||||
@@ -1984,9 +2108,9 @@ void RenderMiningTab(App* app)
|
||||
float badgeX = rMax.x - pad - Layout::spacingXl() * 3.5f;
|
||||
if (mtx.mature) {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
|
||||
WithAlpha(Success(), 180), "Mature");
|
||||
WithAlpha(Success(), 180), TR("mature"));
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%d conf", mtx.confirmations);
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), mtx.confirmations);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
|
||||
WithAlpha(Warning(), 200), buf);
|
||||
}
|
||||
@@ -2001,7 +2125,7 @@ void RenderMiningTab(App* app)
|
||||
dragonx::util::Platform::openUrl(url);
|
||||
}
|
||||
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
|
||||
ImGui::SetTooltip("Open in explorer");
|
||||
ImGui::SetTooltip("%s", TR("mining_open_in_explorer"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2026,8 +2150,8 @@ void RenderMiningTab(App* app)
|
||||
const char* dotIcon = ICON_MD_CIRCLE;
|
||||
ImU32 dotCol = state.pool_mining.connected ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200);
|
||||
const char* statusText = state.pool_mining.connected
|
||||
? (state.pool_mining.pool_url.empty() ? "Connected" : state.pool_mining.pool_url.c_str())
|
||||
: "Connecting...";
|
||||
? (state.pool_mining.pool_url.empty() ? TR("mining_connected") : state.pool_mining.pool_url.c_str())
|
||||
: TR("mining_connecting");
|
||||
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 dotSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, dotIcon);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "peers_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../theme.h"
|
||||
#include "../effects/imgui_acrylic.h"
|
||||
#include "../effects/low_spec.h"
|
||||
@@ -148,7 +149,7 @@ void RenderPeersTab(App* app)
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// Card header
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "BLOCKCHAIN");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_blockchain"));
|
||||
|
||||
float colW = (cardW - pad * 2) / 2.0f;
|
||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||
@@ -165,10 +166,11 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// Blocks
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Blocks");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
|
||||
int blocks = state.sync.blocks;
|
||||
if (blocks > 0) {
|
||||
int blocksLeft = state.sync.headers - blocks;
|
||||
int chainTip = state.longestchain > 0 ? state.longestchain : state.sync.headers;
|
||||
int blocksLeft = chainTip - blocks;
|
||||
if (blocksLeft < 0) blocksLeft = 0;
|
||||
if (blocksLeft > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
|
||||
@@ -180,7 +182,7 @@ void RenderPeersTab(App* app)
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), blockStr);
|
||||
// Draw "(X left)" in warning color
|
||||
char leftStr[32];
|
||||
snprintf(leftStr, sizeof(leftStr), "(%d left)", blocksLeft);
|
||||
snprintf(leftStr, sizeof(leftStr), TR("peers_blocks_left"), blocksLeft);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx + numSz.x, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
|
||||
Warning(), leftStr);
|
||||
@@ -194,7 +196,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Longest Chain
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Longest Chain");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest_chain"));
|
||||
if (state.longestchain > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||
@@ -213,7 +215,7 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// Hashrate
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Hashrate");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (mining.networkHashrate > 0) {
|
||||
if (mining.networkHashrate >= 1e12)
|
||||
@@ -233,7 +235,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Difficulty
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Difficulty");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (mining.difficulty > 0) {
|
||||
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||
@@ -251,7 +253,7 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// Notarized
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Notarized");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.notarized > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.notarized);
|
||||
@@ -262,7 +264,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Protocol
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Protocol");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_protocol"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.protocol_version > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.protocol_version);
|
||||
@@ -280,7 +282,7 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// Version
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Version");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_version"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.daemon_version > 0) {
|
||||
int major = state.daemon_version / 1000000;
|
||||
@@ -294,7 +296,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Memory
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Memory");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_memory"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
double memMb = state.mining.daemon_memory_mb;
|
||||
if (memMb > 0) {
|
||||
@@ -316,7 +318,7 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// Longest Chain
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Longest");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.longestchain > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||
@@ -330,7 +332,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Best Block (truncated hash)
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Best Block");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (!state.sync.best_blockhash.empty()) {
|
||||
// Truncate hash to fit: first 6 + "..." + last 6
|
||||
@@ -349,14 +351,14 @@ void RenderPeersTab(App* app)
|
||||
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Click to copy: %s", hash.c_str());
|
||||
ImGui::SetTooltip("%s %s", TR("peers_click_copy"), hash.c_str());
|
||||
dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp),
|
||||
ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp),
|
||||
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(hash.c_str());
|
||||
ui::Notifications::instance().info("Block hash copied");
|
||||
ui::Notifications::instance().info(TR("peers_hash_copied"));
|
||||
}
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
@@ -373,7 +375,7 @@ void RenderPeersTab(App* app)
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// Card header
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "PEERS");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_upper"));
|
||||
|
||||
float colW = (cardW - pad * 2) / 2.0f;
|
||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||
@@ -390,13 +392,13 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// Connected
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Connected");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_connected"));
|
||||
snprintf(buf, sizeof(buf), "%d", totalPeers);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
|
||||
// In / Out
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "In / Out");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_in_out"));
|
||||
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
}
|
||||
@@ -426,7 +428,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Avg Ping
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Avg Ping");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_avg_ping"));
|
||||
ImU32 pingCol;
|
||||
if (avgPing < 100) pingCol = Success();
|
||||
else if (avgPing < 500) pingCol = Warning();
|
||||
@@ -443,13 +445,13 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// Received
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Received");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_received"));
|
||||
std::string recvStr = fmtBytes(totalBytesRecv);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), recvStr.c_str());
|
||||
|
||||
// Sent
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Sent");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_sent"));
|
||||
std::string sentStr = fmtBytes(totalBytesSent);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sentStr.c_str());
|
||||
}
|
||||
@@ -462,7 +464,7 @@ void RenderPeersTab(App* app)
|
||||
{
|
||||
// P2P Port
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "P2P Port");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_p2p_port"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.p2p_port > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.p2p_port);
|
||||
@@ -472,7 +474,7 @@ void RenderPeersTab(App* app)
|
||||
}
|
||||
// Banned count
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Banned");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_banned"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
size_t bannedCount = state.bannedPeers.size();
|
||||
snprintf(buf, sizeof(buf), "%zu", bannedCount);
|
||||
@@ -488,7 +490,7 @@ void RenderPeersTab(App* app)
|
||||
// Compute remaining space for peer list + footer
|
||||
// ================================================================
|
||||
float footerH = ImGui::GetFrameHeight() + Layout::spacingSm();
|
||||
float toggleH = body2->LegacySize + Layout::spacingMd() * 2;
|
||||
float toggleH = body2->LegacySize + Layout::spacingMd() * 2 + Layout::spacingSm();
|
||||
float remainForPeers = std::max(60.0f, peersAvail.y - (ImGui::GetCursorScreenPos().y - ImGui::GetWindowPos().y) - footerH - Layout::spacingSm());
|
||||
float peerPanelHeight = remainForPeers - toggleH;
|
||||
peerPanelHeight = std::max(S.drawElement("tabs.peers", "peer-panel-min-height").size, peerPanelHeight);
|
||||
@@ -502,8 +504,8 @@ void RenderPeersTab(App* app)
|
||||
float toggleY = ImGui::GetCursorScreenPos().y;
|
||||
{
|
||||
char connLabel[64], banLabel[64];
|
||||
snprintf(connLabel, sizeof(connLabel), "Connected (%zu)", state.peers.size());
|
||||
snprintf(banLabel, sizeof(banLabel), "Banned (%zu)", state.bannedPeers.size());
|
||||
snprintf(connLabel, sizeof(connLabel), TR("peers_connected_count"), state.peers.size());
|
||||
snprintf(banLabel, sizeof(banLabel), TR("peers_banned_count"), state.bannedPeers.size());
|
||||
|
||||
ImVec2 connSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, connLabel);
|
||||
ImVec2 banSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, banLabel);
|
||||
@@ -544,9 +546,14 @@ void RenderPeersTab(App* app)
|
||||
// Refresh button — top-right, glass panel style (similar to mining button)
|
||||
{
|
||||
bool isRefreshing = app->isPeerRefreshInProgress();
|
||||
auto refreshBtn = S.drawElement("tabs.peers", "refresh-button");
|
||||
float btnW = refreshBtn.size;
|
||||
float btnH = toggleH - 4.0f * Layout::dpiScale();
|
||||
ImFont* iconFont = Type().iconMed();
|
||||
float iconSz = iconFont->LegacySize;
|
||||
const char* label = isRefreshing ? TR("peers_refreshing") : TR("peers_refresh");
|
||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||
float padH = Layout::spacingSm();
|
||||
float padV = Layout::spacingSm();
|
||||
float btnW = padH + iconSz + Layout::spacingXs() + lblSz.x + padH;
|
||||
float btnH = std::max(iconSz, lblSz.y) + padV * 2;
|
||||
float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm();
|
||||
float btnY = toggleY + (toggleH - btnH) * 0.5f;
|
||||
ImVec2 bMin(btnX, btnY);
|
||||
@@ -574,10 +581,8 @@ void RenderPeersTab(App* app)
|
||||
}
|
||||
|
||||
// Icon: spinner while refreshing, refresh icon otherwise
|
||||
float cx = bMin.x + btnW * 0.35f;
|
||||
float cx = bMin.x + padH + iconSz * 0.5f;
|
||||
float cy = bMin.y + btnH * 0.5f;
|
||||
ImFont* iconFont = Type().iconMed();
|
||||
float iconSz = iconFont->LegacySize;
|
||||
|
||||
if (isRefreshing) {
|
||||
// Spinning arc spinner (same style as mining toggle)
|
||||
@@ -617,7 +622,6 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Label to the right of icon
|
||||
{
|
||||
const char* label = isRefreshing ? "REFRESHING" : "REFRESH";
|
||||
ImU32 lblCol;
|
||||
if (isRefreshing) {
|
||||
float pulse = effects::isLowSpecMode()
|
||||
@@ -627,7 +631,6 @@ void RenderPeersTab(App* app)
|
||||
} else {
|
||||
lblCol = btnHovered ? OnSurface() : WithAlpha(OnSurface(), 160);
|
||||
}
|
||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||
float lblX = cx + iconSz * 0.5f + Layout::spacingXs();
|
||||
float lblY = cy - lblSz.y * 0.5f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
|
||||
@@ -645,7 +648,7 @@ void RenderPeersTab(App* app)
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (!isRefreshing)
|
||||
ImGui::SetTooltip("Refresh peers & blockchain");
|
||||
ImGui::SetTooltip("%s", TR("peers_refresh_tooltip"));
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -673,10 +676,10 @@ void RenderPeersTab(App* app)
|
||||
// ---- Connected Peers ----
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
|
||||
} else if (state.peers.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No connected peers");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_connected"));
|
||||
} else {
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
|
||||
float rowInset = Layout::spacingLg();
|
||||
@@ -727,7 +730,7 @@ void RenderPeersTab(App* app)
|
||||
}
|
||||
|
||||
{
|
||||
const char* dirLabel = peer.inbound ? "In" : "Out";
|
||||
const char* dirLabel = peer.inbound ? TR("peers_dir_in") : TR("peers_dir_out");
|
||||
ImU32 dirBg = peer.inbound ? WithAlpha(Success(), 30) : WithAlpha(Secondary(), 30);
|
||||
ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200);
|
||||
ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel);
|
||||
@@ -761,12 +764,12 @@ void RenderPeersTab(App* app)
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
|
||||
} else {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2),
|
||||
WithAlpha(Error(), 140), "No TLS");
|
||||
WithAlpha(Error(), 140), TR("peers_no_tls"));
|
||||
}
|
||||
|
||||
if (peer.banscore > 0) {
|
||||
char banBuf[16];
|
||||
snprintf(banBuf, sizeof(banBuf), "Ban: %d", peer.banscore);
|
||||
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
|
||||
ImU32 banCol = peer.banscore > 50 ? Error() : Warning();
|
||||
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
@@ -780,16 +783,16 @@ void RenderPeersTab(App* app)
|
||||
|
||||
const auto& acrylicTheme = GetCurrentAcrylicTheme();
|
||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme.menu)) {
|
||||
ImGui::Text("Peer: %s", peer.addr.c_str());
|
||||
ImGui::Text(TR("peers_peer_label"), peer.addr.c_str());
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Copy Address")) {
|
||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
||||
ImGui::SetClipboardText(peer.addr.c_str());
|
||||
}
|
||||
if (ImGui::MenuItem("Copy IP")) {
|
||||
if (ImGui::MenuItem(TR("peers_copy_ip"))) {
|
||||
ImGui::SetClipboardText(ExtractIP(peer.addr).c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Ban Peer (24h)")) {
|
||||
if (ImGui::MenuItem(TR("peers_ban_24h"))) {
|
||||
app->banPeer(ExtractIP(peer.addr), 86400);
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
@@ -808,18 +811,18 @@ void RenderPeersTab(App* app)
|
||||
};
|
||||
char ttBuf[128];
|
||||
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.id);
|
||||
TTRow("ID", ttBuf);
|
||||
TTRow("Services", peer.services.c_str());
|
||||
TTRow(TR("peers_tt_id"), ttBuf);
|
||||
TTRow(TR("peers_tt_services"), peer.services.c_str());
|
||||
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.startingheight);
|
||||
TTRow("Start Height", ttBuf);
|
||||
TTRow(TR("peers_tt_start_height"), ttBuf);
|
||||
snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytessent);
|
||||
TTRow("Sent", ttBuf);
|
||||
TTRow(TR("peers_tt_sent"), ttBuf);
|
||||
snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytesrecv);
|
||||
TTRow("Received", ttBuf);
|
||||
TTRow(TR("peers_tt_received"), ttBuf);
|
||||
snprintf(ttBuf, sizeof(ttBuf), "%d / %d", peer.synced_headers, peer.synced_blocks);
|
||||
TTRow("Synced H/B", ttBuf);
|
||||
TTRow(TR("peers_tt_synced"), ttBuf);
|
||||
if (!peer.tls_cipher.empty())
|
||||
TTRow("TLS Cipher", peer.tls_cipher.c_str());
|
||||
TTRow(TR("peers_tt_tls_cipher"), peer.tls_cipher.c_str());
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -840,10 +843,10 @@ void RenderPeersTab(App* app)
|
||||
// ---- Banned Peers ----
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
|
||||
} else if (state.bannedPeers.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No banned peers");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_banned"));
|
||||
} else {
|
||||
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size;
|
||||
float rowInsetB = pad;
|
||||
@@ -886,7 +889,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
float btnX = rowPos.x + innerW - Layout::spacingXl() * S.drawElement("tabs.peers", "unban-btn-right-offset-multiplier").size;
|
||||
ImGui::SetCursorScreenPos(ImVec2(btnX, cy - 1));
|
||||
if (TactileSmallButton("Unban", S.resolveFont("button"))) {
|
||||
if (TactileSmallButton(TR("peers_unban"), S.resolveFont("button"))) {
|
||||
app->unbanPeer(banned.address);
|
||||
}
|
||||
|
||||
@@ -898,10 +901,10 @@ void RenderPeersTab(App* app)
|
||||
|
||||
const auto& acrylicTheme2 = GetCurrentAcrylicTheme();
|
||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) {
|
||||
if (ImGui::MenuItem("Copy Address")) {
|
||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
||||
ImGui::SetClipboardText(banned.address.c_str());
|
||||
}
|
||||
if (ImGui::MenuItem("Unban")) {
|
||||
if (ImGui::MenuItem(TR("peers_unban"))) {
|
||||
app->unbanPeer(banned.address);
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
@@ -940,7 +943,7 @@ void RenderPeersTab(App* app)
|
||||
// ================================================================
|
||||
if (s_show_banned && !state.bannedPeers.empty()) {
|
||||
ImGui::BeginDisabled(!app->isConnected());
|
||||
if (TactileSmallButton("Clear All Bans", S.resolveFont("button"))) {
|
||||
if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button"))) {
|
||||
app->clearBans();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "qr_popup_dialog.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../widgets/qr_code.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -63,7 +64,7 @@ void QRPopupDialog::render(App* app)
|
||||
auto addrInput = S.input("dialogs.qr-popup", "address-input");
|
||||
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
|
||||
|
||||
if (material::BeginOverlayDialog("QR Code", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("qr_title"), &s_open, win.width, 0.94f)) {
|
||||
|
||||
// Label if present
|
||||
if (!s_label.empty()) {
|
||||
@@ -86,7 +87,7 @@ void QRPopupDialog::render(App* app)
|
||||
} else {
|
||||
// Fallback: show error
|
||||
ImGui::BeginChild("QRPlaceholder", ImVec2(qr_size, qr_size), true);
|
||||
ImGui::TextWrapped("Failed to generate QR code");
|
||||
ImGui::TextWrapped("%s", TR("qr_failed"));
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ void QRPopupDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Address display
|
||||
ImGui::Text("Address:");
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
|
||||
// Use multiline for z-addresses
|
||||
if (s_address.length() > 50) {
|
||||
@@ -120,13 +121,13 @@ void QRPopupDialog::render(App* app)
|
||||
float start_x = (window_width - total_width) / 2.0f;
|
||||
ImGui::SetCursorPosX(start_x);
|
||||
|
||||
if (material::StyledButton("Copy Address", ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("copy_address"), ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
|
||||
ImGui::SetClipboardText(s_address.c_str());
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
|
||||
close();
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "receive_tab.h"
|
||||
#include "send_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../ui/widgets/qr_code.h"
|
||||
@@ -35,6 +36,10 @@ namespace ui {
|
||||
|
||||
using namespace material;
|
||||
|
||||
static std::string TrId(const char* key, const char* id) {
|
||||
return std::string(TR(key)) + "##" + id;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
@@ -110,7 +115,7 @@ static void RenderSyncBanner(const WalletState& state) {
|
||||
? (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);
|
||||
TR("blockchain_syncing"), syncPct);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.receive", "sync-banner-bg-color").color)));
|
||||
float syncH = std::max(schema::UI().drawElement("tabs.receive", "sync-banner-min-height").size, schema::UI().drawElement("tabs.receive", "sync-banner-height").size * Layout::vScale());
|
||||
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, syncH),
|
||||
@@ -129,13 +134,13 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
char buf[256];
|
||||
|
||||
// Header row: label + address type toggle buttons
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("address_upper"));
|
||||
|
||||
float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width));
|
||||
float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
|
||||
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
|
||||
ImGui::SameLine(width - toggleTotalW);
|
||||
const char* filterLabels[] = { "All", "Z", "T" };
|
||||
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
bool isActive = (s_addr_type_filter == i);
|
||||
if (isActive) {
|
||||
@@ -190,7 +195,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
|
||||
// Build preview string
|
||||
if (!app->isConnected()) {
|
||||
s_source_preview = "Not connected to daemon";
|
||||
s_source_preview = TR("not_connected");
|
||||
} else if (s_selected_address_idx >= 0 &&
|
||||
s_selected_address_idx < (int)state.addresses.size()) {
|
||||
const auto& addr = state.addresses[s_selected_address_idx];
|
||||
@@ -202,7 +207,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
s_source_preview = buf;
|
||||
} else {
|
||||
s_source_preview = "Select a receiving address...";
|
||||
s_source_preview = TR("select_receiving_address");
|
||||
}
|
||||
|
||||
float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
|
||||
@@ -212,7 +217,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
|
||||
if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) {
|
||||
if (!app->isConnected() || state.addresses.empty()) {
|
||||
ImGui::TextDisabled("No addresses available");
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_available"));
|
||||
} else {
|
||||
// Build filtered and sorted list
|
||||
std::vector<size_t> sortedIdx;
|
||||
@@ -229,7 +234,7 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
});
|
||||
|
||||
if (sortedIdx.empty()) {
|
||||
ImGui::TextDisabled("No addresses match filter");
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_match"));
|
||||
} else {
|
||||
size_t addrTruncLen = static_cast<size_t>(std::max(schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-divisor").size));
|
||||
double now = ImGui::GetTime();
|
||||
@@ -287,10 +292,10 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::BeginDisabled(!app->isConnected() || s_selected_address_idx < 0 ||
|
||||
s_selected_address_idx >= (int)state.addresses.size());
|
||||
if (TactileButton("Copy##recvAddr", ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TrId("copy", "recvAddr").c_str(), ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
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");
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
@@ -298,23 +303,23 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
// New address button on same line
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::BeginDisabled(!app->isConnected());
|
||||
if (TactileButton("+ New##recv", ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 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");
|
||||
Notifications::instance().error(TR("failed_create_shielded"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success("New shielded address created");
|
||||
Notifications::instance().success(TR("new_shielded_created"));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
if (addr.empty())
|
||||
Notifications::instance().error("Failed to create new transparent address");
|
||||
Notifications::instance().error(TR("failed_create_transparent"));
|
||||
else {
|
||||
s_pending_select_address = addr;
|
||||
Notifications::instance().success("New transparent address created");
|
||||
Notifications::instance().success(TR("new_transparent_created"));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -330,10 +335,11 @@ static std::string recvTimeAgo(int64_t timestamp) {
|
||||
int64_t now = (int64_t)std::time(nullptr);
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60) return std::to_string(diff) + "s ago";
|
||||
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
|
||||
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
|
||||
return std::to_string(diff / 86400) + "d ago";
|
||||
char buf[32];
|
||||
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
|
||||
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
|
||||
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
|
||||
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
|
||||
}
|
||||
|
||||
static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) {
|
||||
@@ -353,7 +359,7 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
const WalletState& state, float width,
|
||||
ImFont* capFont, App* app) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT RECEIVED");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_received"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float hs = Layout::hScale(width);
|
||||
@@ -375,7 +381,7 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
}
|
||||
|
||||
if (recvs.empty()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No recent receives");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -416,11 +422,11 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
|
||||
// Type label (first line)
|
||||
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, "Received");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, TR("received_label"));
|
||||
|
||||
// Time (next to type)
|
||||
std::string ago = recvTimeAgo(tx.timestamp);
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "Received").x;
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("received_label")).x;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
@@ -457,12 +463,12 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
statusStr = "Pending"; statusCol = Warning();
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < (int)schema::UI().drawElement("tabs.receive", "confirmed-threshold").size) {
|
||||
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
statusStr = buf; statusCol = Warning();
|
||||
} else {
|
||||
statusStr = "Confirmed"; statusCol = greenCol;
|
||||
statusStr = TR("confirmed"); statusCol = greenCol;
|
||||
}
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
@@ -545,10 +551,10 @@ void RenderReceiveTab(App* app)
|
||||
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
|
||||
OnSurfaceDisabled(), "Waiting for daemon connection...");
|
||||
OnSurfaceDisabled(), TR("waiting_for_daemon"));
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + S.drawElement("tabs.receive", "empty-state-subtitle-gap").size),
|
||||
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
|
||||
OnSurfaceDisabled(), TR("addresses_appear_here"));
|
||||
ImGui::Dummy(ImVec2(formW, emptyH));
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndChild();
|
||||
@@ -572,7 +578,7 @@ void RenderReceiveTab(App* app)
|
||||
skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size),
|
||||
OnSurfaceDisabled(), "Loading addresses...");
|
||||
OnSurfaceDisabled(), TR("loading_addresses"));
|
||||
ImGui::Dummy(ImVec2(formW, emptyH));
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndChild();
|
||||
@@ -639,7 +645,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// ---- PAYMENT REQUEST ----
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Amount input with currency toggle
|
||||
@@ -805,7 +811,7 @@ void RenderReceiveTab(App* app)
|
||||
// Memo (z-addresses only)
|
||||
if (isZ) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (OPTIONAL)");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));
|
||||
|
||||
float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
|
||||
@@ -841,7 +847,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
|
||||
ImGui::BeginDisabled(!hasData);
|
||||
if (TactileSmallButton("Clear Request##recv", S.resolveFont("button"))) {
|
||||
if (TactileSmallButton(TrId("clear_request", "recv").c_str(), S.resolveFont("button"))) {
|
||||
s_request_amount = 0.0;
|
||||
s_request_usd_amount = 0.0;
|
||||
s_request_memo[0] = '\0';
|
||||
@@ -880,20 +886,20 @@ void RenderReceiveTab(App* app)
|
||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
|
||||
qrPanelMin.y + totalQrSize * 0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, textPos,
|
||||
OnSurfaceDisabled(), "QR unavailable");
|
||||
OnSurfaceDisabled(), TR("qr_unavailable"));
|
||||
}
|
||||
|
||||
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");
|
||||
ImGui::SetTooltip("%s",
|
||||
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImGui::SetClipboardText(qr_data.c_str());
|
||||
Notifications::instance().info(s_request_amount > 0
|
||||
? "Payment URI copied" : "Address copied");
|
||||
? TR("payment_uri_copied") : TR("address_copied"));
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(rx, qrPanelMax.y));
|
||||
@@ -926,9 +932,9 @@ void RenderReceiveTab(App* app)
|
||||
if (s_request_amount > 0) {
|
||||
if (!firstBtn) ImGui::SameLine(0, btnGap);
|
||||
firstBtn = false;
|
||||
if (TactileButton("Copy URI##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("copy_uri", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_cached_qr_data.c_str());
|
||||
Notifications::instance().info("Payment URI copied");
|
||||
Notifications::instance().info(TR("payment_uri_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -939,14 +945,16 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
||||
if (TactileButton("Share##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("share", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
char shareBuf[1024];
|
||||
snprintf(shareBuf, sizeof(shareBuf),
|
||||
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
|
||||
"%s\n%s: %.8f %s\n%s: %s\nURI: %s",
|
||||
TR("payment_request"), TR("amount"),
|
||||
s_request_amount, DRAGONX_TICKER,
|
||||
selected.address.c_str(), s_cached_qr_data.c_str());
|
||||
TR("address"), selected.address.c_str(),
|
||||
s_cached_qr_data.c_str());
|
||||
ImGui::SetClipboardText(shareBuf);
|
||||
Notifications::instance().info("Payment request copied");
|
||||
Notifications::instance().info(TR("payment_request_copied"));
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
@@ -959,7 +967,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
||||
if (TactileButton("Explorer##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
OpenExplorerURL(selected.address);
|
||||
}
|
||||
ImGui::PopStyleVar(); // FrameBorderSize
|
||||
@@ -972,7 +980,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
|
||||
if (TactileButton("Send \xe2\x86\x97##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("send", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
SetSendFromAddress(selected.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
@@ -1001,7 +1009,7 @@ void RenderReceiveTab(App* app)
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
|
||||
ImGui::Dummy(ImVec2(formW, 0));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
@@ -128,22 +128,19 @@ void RequestPaymentDialog::render(App* app)
|
||||
auto qr = S.drawElement("dialogs.request-payment", "qr-code");
|
||||
auto actionBtn = S.button("dialogs.request-payment", "action-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Request Payment", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("request_title"), &s_open, win.width, 0.94f)) {
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Generate a payment request that others can scan or copy. "
|
||||
"The QR code contains your address and optional amount/memo."
|
||||
);
|
||||
ImGui::TextWrapped("%s", TR("request_description"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Address selection
|
||||
ImGui::Text("Receive Address:");
|
||||
ImGui::Text("%s", TR("request_receive_address"));
|
||||
|
||||
std::string addr_display = s_address[0] ? s_address : "Select address...";
|
||||
std::string addr_display = s_address[0] ? s_address : TR("request_select_address");
|
||||
if (addr_display.length() > static_cast<size_t>(zAddrLbl.truncate)) {
|
||||
addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate);
|
||||
}
|
||||
@@ -152,7 +149,7 @@ void RequestPaymentDialog::render(App* app)
|
||||
if (ImGui::BeginCombo("##Address", addr_display.c_str())) {
|
||||
// Z-addresses
|
||||
if (!state.z_addresses.empty()) {
|
||||
ImGui::TextDisabled("-- Shielded Addresses --");
|
||||
ImGui::TextDisabled("%s", TR("request_shielded_addrs"));
|
||||
for (size_t i = 0; i < state.z_addresses.size(); i++) {
|
||||
const auto& addr = state.z_addresses[i];
|
||||
std::string label = addr.address;
|
||||
@@ -169,7 +166,7 @@ void RequestPaymentDialog::render(App* app)
|
||||
|
||||
// T-addresses
|
||||
if (!state.t_addresses.empty()) {
|
||||
ImGui::TextDisabled("-- Transparent Addresses --");
|
||||
ImGui::TextDisabled("%s", TR("request_transparent_addrs"));
|
||||
for (size_t i = 0; i < state.t_addresses.size(); i++) {
|
||||
const auto& addr = state.t_addresses[i];
|
||||
std::string label = addr.address;
|
||||
@@ -189,7 +186,7 @@ void RequestPaymentDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Amount (optional)
|
||||
ImGui::Text("Amount (optional):");
|
||||
ImGui::Text("%s", TR("request_amount"));
|
||||
ImGui::SetNextItemWidth(amountInput.width);
|
||||
if (ImGui::InputDouble("##Amount", &s_amount, 0.1, 1.0, "%.8f")) {
|
||||
s_uri_dirty = true;
|
||||
@@ -200,7 +197,7 @@ void RequestPaymentDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Label (optional)
|
||||
ImGui::Text("Label (optional):");
|
||||
ImGui::Text("%s", TR("request_label"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::InputText("##Label", s_label, sizeof(s_label))) {
|
||||
s_uri_dirty = true;
|
||||
@@ -211,7 +208,7 @@ void RequestPaymentDialog::render(App* app)
|
||||
// Memo (optional, only for z-addr)
|
||||
bool is_zaddr = (s_address[0] == 'z');
|
||||
if (is_zaddr) {
|
||||
ImGui::Text("Memo (optional):");
|
||||
ImGui::Text("%s", TR("request_memo"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo), ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) {
|
||||
s_uri_dirty = true;
|
||||
@@ -245,7 +242,7 @@ void RequestPaymentDialog::render(App* app)
|
||||
|
||||
// Payment URI display
|
||||
if (!s_payment_uri.empty()) {
|
||||
ImGui::Text("Payment URI:");
|
||||
ImGui::Text("%s", TR("request_payment_uri"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
|
||||
// Use a selectable text area for the URI
|
||||
@@ -256,23 +253,23 @@ void RequestPaymentDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Copy button
|
||||
if (material::StyledButton("Copy URI", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("request_copy_uri"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
ImGui::SetClipboardText(s_payment_uri.c_str());
|
||||
Notifications::instance().success("Payment URI copied to clipboard");
|
||||
Notifications::instance().success(TR("request_uri_copied"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton("Copy Address", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("copy_address"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
ImGui::SetClipboardText(s_address);
|
||||
Notifications::instance().success("Address copied to clipboard");
|
||||
Notifications::instance().success(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Close button
|
||||
if (material::StyledButton("Close", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
@@ -214,7 +214,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
auto& S = schema::UI();
|
||||
char buf[256];
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SENDING FROM");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_sending_from"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Auto-select the address with the largest balance on first load
|
||||
@@ -237,7 +237,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
|
||||
// Build preview string for selected address
|
||||
if (!app->isConnected()) {
|
||||
s_source_preview = "Not connected to daemon";
|
||||
s_source_preview = TR("not_connected");
|
||||
} else if (s_selected_from_idx >= 0 &&
|
||||
s_selected_from_idx < (int)state.addresses.size()) {
|
||||
const auto& addr = state.addresses[s_selected_from_idx];
|
||||
@@ -249,7 +249,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
s_source_preview = buf;
|
||||
} else {
|
||||
s_source_preview = "Select a source address...";
|
||||
s_source_preview = TR("send_select_source");
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(width);
|
||||
@@ -257,7 +257,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
|
||||
if (ImGui::BeginCombo("##SendFrom", s_source_preview.c_str())) {
|
||||
if (!app->isConnected() || state.addresses.empty()) {
|
||||
ImGui::TextDisabled("No addresses available");
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_available"));
|
||||
} else {
|
||||
// Sort by balance descending, only show addresses with balance
|
||||
std::vector<size_t> sortedIdx;
|
||||
@@ -272,7 +272,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
});
|
||||
|
||||
if (sortedIdx.empty()) {
|
||||
ImGui::TextDisabled("No addresses with balance");
|
||||
ImGui::TextDisabled("%s", TR("send_no_balance"));
|
||||
} else {
|
||||
size_t addrTruncLen = static_cast<size_t>(std::max(S.drawElement("tabs.send", "addr-dropdown-trunc-min").size, width / S.drawElement("tabs.send", "addr-dropdown-trunc-divisor").size));
|
||||
|
||||
@@ -352,7 +352,7 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
||||
// ============================================================================
|
||||
static void RenderFeeTierSelector(const char* suffix = "") {
|
||||
auto& S = schema::UI();
|
||||
const char* feeLabels[] = { "Low", "Normal", "High" };
|
||||
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
|
||||
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
@@ -569,12 +569,12 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-bg-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-hover-alpha").size)));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size);
|
||||
if (TactileSmallButton("Copy Error##txErr", schema::UI().resolveFont("button"))) {
|
||||
if (TactileSmallButton(TR("send_copy_error"), schema::UI().resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_tx_status.c_str());
|
||||
Notifications::instance().info("Error copied to clipboard");
|
||||
Notifications::instance().info(TR("send_error_copied"));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton("Dismiss##txErr", schema::UI().resolveFont("button"))) {
|
||||
if (TactileSmallButton(TR("send_dismiss"), schema::UI().resolveFont("button"))) {
|
||||
s_tx_status.clear();
|
||||
s_result_txid.clear();
|
||||
s_status_success = false;
|
||||
@@ -615,7 +615,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(ix, iy), Primary(), spinIcon);
|
||||
double elapsed = ImGui::GetTime() - s_send_start_time;
|
||||
snprintf(buf, sizeof(buf), "Submitting transaction... (%.0fs)", elapsed);
|
||||
snprintf(buf, sizeof(buf), TR("send_submitting"), elapsed);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), OnSurface(), buf);
|
||||
} else {
|
||||
// Success checkmark
|
||||
@@ -624,7 +624,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, checkIcon);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(ix, iy), Success(), checkIcon);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), "Transaction sent!");
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), TR("send_tx_sent"));
|
||||
|
||||
if (!s_result_txid.empty()) {
|
||||
float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size;
|
||||
@@ -633,13 +633,13 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
||||
std::string dispTxid = (int)s_result_txid.length() > txidThreshold
|
||||
? s_result_txid.substr(0, txidTruncLen) + "..." + s_result_txid.substr(s_result_txid.length() - txidTruncLen)
|
||||
: s_result_txid;
|
||||
snprintf(buf, sizeof(buf), "TxID: %s", dispTxid.c_str());
|
||||
snprintf(buf, sizeof(buf), TR("send_txid_label"), dispTxid.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size, txY),
|
||||
OnSurfaceDisabled(), buf);
|
||||
ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size));
|
||||
if (TactileSmallButton("Copy##TxID", schema::UI().resolveFont("button"))) {
|
||||
if (TactileSmallButton(TR("copy"), schema::UI().resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(s_result_txid.c_str());
|
||||
Notifications::instance().info("TxID copied to clipboard");
|
||||
Notifications::instance().info(TR("send_txid_copied"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,7 +688,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
|
||||
// FROM card
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "FROM");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
|
||||
ImVec2 cMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
|
||||
GlassPanelSpec gs; gs.rounding = popGlassRound;
|
||||
@@ -702,7 +702,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
|
||||
// TO card
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TO");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("to_upper"));
|
||||
ImVec2 cMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
|
||||
GlassPanelSpec gs; gs.rounding = popGlassRound;
|
||||
@@ -716,7 +716,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
|
||||
// Fee tier selector
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NETWORK FEE");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_network_fee"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
RenderFeeTierSelector("##confirm");
|
||||
// Recalculate total after potential fee change
|
||||
@@ -729,7 +729,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
float valX = std::max(schema::UI().drawElement("tabs.send", "confirm-val-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-val-col-x").size * popVs);
|
||||
float usdX = popW - std::max(schema::UI().drawElement("tabs.send", "confirm-usd-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-usd-col-x").size * popVs);
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT DETAILS");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details"));
|
||||
ImVec2 cMin = ImGui::GetCursorScreenPos();
|
||||
float cH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
|
||||
ImVec2 cMax(cMin.x + popW, cMin.y + cH);
|
||||
@@ -740,7 +740,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
float cy = cMin.y + Layout::spacingSm() + Layout::spacingXs();
|
||||
float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
|
||||
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Amount");
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_amount"));
|
||||
snprintf(buf, sizeof(buf), "%.8f %s", s_amount, DRAGONX_TICKER);
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
|
||||
if (market.price_usd > 0) {
|
||||
@@ -749,7 +749,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
}
|
||||
cy += rowStep;
|
||||
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Fee");
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_fee"));
|
||||
snprintf(buf, sizeof(buf), "%.8f %s", s_fee, DRAGONX_TICKER);
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
|
||||
if (market.price_usd > 0) {
|
||||
@@ -762,7 +762,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
ImGui::GetColorU32(Divider()), S.drawElement("tabs.send", "confirm-divider-thickness").size);
|
||||
cy += rowStep;
|
||||
|
||||
popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Total");
|
||||
popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_total"));
|
||||
snprintf(buf, sizeof(buf), "%.8f %s", total, DRAGONX_TICKER);
|
||||
DrawTextShadow(popDl, sub1, sub1->LegacySize, ImVec2(cx + valX, cy), Primary(), buf);
|
||||
if (market.price_usd > 0) {
|
||||
@@ -774,7 +774,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
}
|
||||
|
||||
if (s_memo[0] != '\0' && is_valid_z) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
|
||||
Type().textColored(TypeStyle::Caption, OnSurface(), s_memo);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
@@ -782,7 +782,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
if (s_sending) {
|
||||
Type().text(TypeStyle::Body2, "Sending...");
|
||||
Type().text(TypeStyle::Body2, TR("sending"));
|
||||
} else {
|
||||
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
|
||||
s_sending = true;
|
||||
@@ -801,26 +801,26 @@ void RenderSendConfirmPopup(App* app) {
|
||||
s_sending = false;
|
||||
s_status_timestamp = ImGui::GetTime();
|
||||
if (success) {
|
||||
s_tx_status = "Transaction sent!";
|
||||
s_tx_status = TR("send_tx_sent");
|
||||
s_result_txid = result;
|
||||
s_status_success = true;
|
||||
Notifications::instance().success("Transaction sent successfully!");
|
||||
Notifications::instance().success(TR("send_tx_success"));
|
||||
s_to_address[0] = '\0';
|
||||
s_amount = 0.0;
|
||||
s_memo[0] = '\0';
|
||||
s_send_max = false;
|
||||
} else {
|
||||
s_tx_status = "Error: " + result;
|
||||
s_tx_status = std::string(TR("send_error_prefix")) + result;
|
||||
s_result_txid.clear();
|
||||
s_status_success = false;
|
||||
Notifications::instance().error("Transaction failed: " + result);
|
||||
Notifications::instance().error(std::string(TR("send_tx_failed")) + result);
|
||||
}
|
||||
}
|
||||
);
|
||||
s_show_confirm = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton("Cancel", ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
|
||||
if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
|
||||
s_show_confirm = false;
|
||||
}
|
||||
}
|
||||
@@ -851,13 +851,13 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
|
||||
|
||||
float cx = ctaMin.x + Layout::spacingXl();
|
||||
float cy = ctaMin.y + Layout::spacingLg();
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), "Your wallet is empty");
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), TR("send_wallet_empty"));
|
||||
cy += sub1->LegacySize + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(),
|
||||
"Switch to Receive to get your address and start receiving funds.");
|
||||
TR("send_switch_to_receive"));
|
||||
cy += capFont->LegacySize + Layout::spacingMd();
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
if (TactileButton("Go to Receive", ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) {
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(ctaMin.x, ctaMax.y + Layout::spacingLg()));
|
||||
@@ -904,19 +904,19 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
|
||||
if (!can_send && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||
if (!app->isConnected())
|
||||
ImGui::SetTooltip("Not connected to daemon");
|
||||
ImGui::SetTooltip("%s", TR("send_tooltip_not_connected"));
|
||||
else if (state.sync.syncing)
|
||||
ImGui::SetTooltip("Blockchain is still syncing");
|
||||
ImGui::SetTooltip("%s", TR("send_tooltip_syncing"));
|
||||
else if (s_from_address[0] == '\0')
|
||||
ImGui::SetTooltip("Select a source address above");
|
||||
ImGui::SetTooltip("%s", TR("send_tooltip_select_source"));
|
||||
else if (!is_valid_address)
|
||||
ImGui::SetTooltip("Enter a valid recipient address");
|
||||
ImGui::SetTooltip("%s", TR("send_tooltip_invalid_address"));
|
||||
else if (s_amount <= 0)
|
||||
ImGui::SetTooltip("Enter an amount to send");
|
||||
ImGui::SetTooltip("%s", TR("send_tooltip_enter_amount"));
|
||||
else if (total > available)
|
||||
ImGui::SetTooltip("Amount exceeds available balance");
|
||||
ImGui::SetTooltip("%s", TR("send_tooltip_exceeds_balance"));
|
||||
else if (s_sending)
|
||||
ImGui::SetTooltip("Transaction in progress...");
|
||||
ImGui::SetTooltip("%s", TR("send_tooltip_in_progress"));
|
||||
}
|
||||
if (!can_send) ImGui::PopStyleColor(3);
|
||||
|
||||
@@ -946,14 +946,14 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
s_clear_confirm_pending = false;
|
||||
}
|
||||
if (ImGui::BeginPopup(confirmClearId)) {
|
||||
ImGui::Text("Clear all form fields?");
|
||||
ImGui::Text("%s", TR("send_clear_fields"));
|
||||
ImGui::Spacing();
|
||||
if (TactileButton("Yes, Clear", ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
|
||||
ClearFormWithUndo();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton("Keep", ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -972,7 +972,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
|
||||
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
|
||||
RestoreFormSnapshot();
|
||||
Notifications::instance().info("Form restored");
|
||||
Notifications::instance().info(TR("send_form_restored"));
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -989,7 +989,7 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
float width, ImFont* capFont, App* app) {
|
||||
auto& S = schema::UI();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT SENDS");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recent_sends"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
@@ -1012,7 +1012,7 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
}
|
||||
|
||||
if (sends.empty()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No recent sends");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1055,11 +1055,11 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
|
||||
// Type label (first line)
|
||||
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, "Sent");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, TR("sent_upper"));
|
||||
|
||||
// Time (next to type)
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "Sent").x;
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("sent_upper")).x;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
@@ -1096,12 +1096,12 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
statusStr = "Pending"; statusCol = Warning();
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < (int)S.drawElement("tabs.send", "confirmed-threshold").size) {
|
||||
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
statusStr = buf; statusCol = Warning();
|
||||
} else {
|
||||
statusStr = "Confirmed"; statusCol = greenCol;
|
||||
statusStr = TR("confirmed"); statusCol = greenCol;
|
||||
}
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
@@ -1238,7 +1238,7 @@ void RenderSendTab(App* app)
|
||||
static bool s_paste_previewing = false;
|
||||
static std::string s_preview_text;
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECIPIENT");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recipient"));
|
||||
|
||||
// Validation indicator — inline next to title (no height change)
|
||||
// Check the preview text during hover, otherwise check actual address
|
||||
@@ -1254,9 +1254,9 @@ void RenderSendTab(App* app)
|
||||
if (vz || vt) {
|
||||
ImGui::SameLine();
|
||||
if (vz)
|
||||
Type().textColored(TypeStyle::Caption, Success(), "Valid shielded address");
|
||||
Type().textColored(TypeStyle::Caption, Success(), TR("send_valid_shielded"));
|
||||
else
|
||||
Type().textColored(TypeStyle::Caption, Warning(), "Valid transparent address");
|
||||
Type().textColored(TypeStyle::Caption, Warning(), TR("send_valid_transparent"));
|
||||
} else if (!checkPreview) {
|
||||
ImGui::SameLine();
|
||||
Type().textColored(TypeStyle::Caption, Error(), TR("invalid_address"));
|
||||
@@ -1341,7 +1341,7 @@ void RenderSendTab(App* app)
|
||||
|
||||
// ---- AMOUNT ----
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_upper"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Toggle between DRGX and USD input
|
||||
@@ -1439,7 +1439,7 @@ void RenderSendTab(App* app)
|
||||
|
||||
// Amount error
|
||||
if (s_amount > 0 && s_amount + s_fee > available && available > 0) {
|
||||
snprintf(buf, sizeof(buf), "Exceeds available (%.8f)", available - s_fee);
|
||||
snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
|
||||
Type().textColored(TypeStyle::Caption, Error(), buf);
|
||||
}
|
||||
}
|
||||
@@ -1455,7 +1455,7 @@ void RenderSendTab(App* app)
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (OPTIONAL)");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.send", "memo-label-gap").size));
|
||||
|
||||
float memoInputH = std::max(S.drawElement("tabs.send", "memo-min-height").size, S.drawElement("tabs.send", "memo-base-height").size * vScale);
|
||||
@@ -1502,7 +1502,7 @@ void RenderSendTab(App* app)
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
|
||||
ImGui::Dummy(ImVec2(formW, 0));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Pass card bottom Y so error overlay can anchor to it
|
||||
float cardBottom = containerMax.y;
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
|
||||
static std::string TrId(const char* tr_key, const char* id) {
|
||||
std::string s = TR(tr_key);
|
||||
s += "##";
|
||||
s += id;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Settings state - these get loaded from Settings on window open
|
||||
static bool s_initialized = false;
|
||||
static int s_language_index = 0;
|
||||
@@ -141,17 +149,17 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
auto saveBtn = S.button("dialogs.settings", "save-button");
|
||||
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
|
||||
|
||||
if (!material::BeginOverlayDialog("Settings", p_open, win.width, 0.94f)) {
|
||||
if (!material::BeginOverlayDialog(TR("settings"), p_open, win.width, 0.94f)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("SettingsTabs")) {
|
||||
// General settings tab
|
||||
if (ImGui::BeginTabItem("General")) {
|
||||
if (ImGui::BeginTabItem(TR("general"))) {
|
||||
ImGui::Spacing();
|
||||
|
||||
// Skin/theme selection
|
||||
ImGui::Text("Theme:");
|
||||
ImGui::Text("%s", TR("theme"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
|
||||
// Active skin combo (populated from SkinManager)
|
||||
@@ -172,7 +180,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::SetNextItemWidth(cmb.width);
|
||||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||||
// Bundled themes header
|
||||
ImGui::TextDisabled("Built-in");
|
||||
ImGui::TextDisabled("%s", TR("settings_builtin"));
|
||||
ImGui::Separator();
|
||||
for (size_t i = 0; i < skins.size(); i++) {
|
||||
const auto& skin = skins[i];
|
||||
@@ -198,7 +206,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
}
|
||||
if (has_custom) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Custom");
|
||||
ImGui::TextDisabled("%s", TR("settings_custom"));
|
||||
ImGui::Separator();
|
||||
for (size_t i = 0; i < skins.size(); i++) {
|
||||
const auto& skin = skins[i];
|
||||
@@ -236,7 +244,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Hotkey: Ctrl+Left/Right to cycle themes");
|
||||
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
|
||||
|
||||
// Show indicator if custom theme is active
|
||||
if (active_is_custom) {
|
||||
@@ -245,7 +253,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Custom theme active");
|
||||
ImGui::SetTooltip("%s", TR("tt_custom_theme"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,14 +265,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
}
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
|
||||
ImGui::SetTooltip(TR("tt_scan_themes"),
|
||||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Language selection
|
||||
ImGui::Text("Language:");
|
||||
ImGui::Text("%s", TR("language"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
auto& i18n = util::I18n::instance();
|
||||
const auto& languages = i18n.getAvailableLanguages();
|
||||
@@ -283,17 +291,17 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
std::advance(it, s_language_index);
|
||||
i18n.loadLanguage(it->first);
|
||||
}
|
||||
ImGui::TextDisabled(" Note: Some text requires restart to update");
|
||||
ImGui::TextDisabled(" %s", TR("settings_language_note"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Acrylic Effects settings
|
||||
ImGui::Text("Visual Effects");
|
||||
ImGui::Text("%s", TR("settings_visual_effects"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Acrylic Level:");
|
||||
ImGui::Text("%s", TR("settings_acrylic_level"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::SetNextItemWidth(cmb.width);
|
||||
{
|
||||
@@ -309,10 +317,10 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount);
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled(" Blur amount (0%% = off, 100%% = maximum)");
|
||||
ImGui::TextDisabled(" %s", TR("tt_blur"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Noise Opacity:");
|
||||
ImGui::Text("%s", TR("settings_noise_opacity"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::SetNextItemWidth(cmb.width);
|
||||
{
|
||||
@@ -326,86 +334,86 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity);
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled(" Grain texture intensity (0%% = off, 100%% = maximum)");
|
||||
ImGui::TextDisabled(" %s", TR("tt_noise"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Accessibility: Reduced transparency
|
||||
if (ImGui::Checkbox("Reduce transparency", &s_reduced_transparency)) {
|
||||
if (ImGui::Checkbox(TrId("settings_reduce_transparency", "reduce_trans").c_str(), &s_reduced_transparency)) {
|
||||
effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency);
|
||||
}
|
||||
ImGui::TextDisabled(" Use solid colors instead of blur effects (accessibility)");
|
||||
ImGui::TextDisabled(" %s", TR("settings_solid_colors_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Checkbox("Simple background", &s_gradient_background)) {
|
||||
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_gradient_background)) {
|
||||
schema::SkinManager::instance().setGradientMode(s_gradient_background);
|
||||
}
|
||||
ImGui::TextDisabled(" Replace textured backgrounds with smooth gradients");
|
||||
ImGui::TextDisabled(" %s", TR("settings_gradient_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Privacy settings
|
||||
ImGui::Text("Privacy");
|
||||
ImGui::Text("%s", TR("settings_privacy"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Checkbox("Save shielded transaction history locally", &s_save_ztxs);
|
||||
ImGui::TextDisabled(" Stores z-addr transactions in a local file for viewing");
|
||||
ImGui::Checkbox(TrId("settings_save_shielded_local", "save_ztx_w").c_str(), &s_save_ztxs);
|
||||
ImGui::TextDisabled(" %s", TR("settings_save_shielded_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Checkbox("Auto-shield transparent funds", &s_auto_shield);
|
||||
ImGui::TextDisabled(" Automatically move transparent funds to shielded addresses");
|
||||
ImGui::Checkbox(TrId("settings_auto_shield_funds", "auto_shld_w").c_str(), &s_auto_shield);
|
||||
ImGui::TextDisabled(" %s", TR("settings_auto_shield_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Checkbox("Use Tor for network connections", &s_use_tor);
|
||||
ImGui::TextDisabled(" Route all connections through Tor for enhanced privacy");
|
||||
ImGui::Checkbox(TrId("settings_use_tor_network", "tor_w").c_str(), &s_use_tor);
|
||||
ImGui::TextDisabled(" %s", TR("settings_tor_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Other settings
|
||||
ImGui::Text("Other");
|
||||
ImGui::Text("%s", TR("settings_other"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Checkbox("Allow custom transaction fees", &s_allow_custom_fees);
|
||||
ImGui::Checkbox("Fetch price data from CoinGecko", &s_fetch_prices);
|
||||
ImGui::Checkbox(TrId("custom_fees", "fees_w").c_str(), &s_allow_custom_fees);
|
||||
ImGui::Checkbox(TrId("fetch_prices", "prices_w").c_str(), &s_fetch_prices);
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// Connection settings tab
|
||||
if (ImGui::BeginTabItem("Connection")) {
|
||||
if (ImGui::BeginTabItem(TR("settings_connection"))) {
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("RPC Connection");
|
||||
ImGui::TextDisabled("Configure connection to dragonxd daemon");
|
||||
ImGui::Text("%s", TR("settings_rpc_connection"));
|
||||
ImGui::TextDisabled("%s", TR("settings_configure_rpc"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Host:");
|
||||
ImGui::Text("%s", TR("rpc_host"));
|
||||
ImGui::SameLine(connLbl.position);
|
||||
ImGui::SetNextItemWidth(cmb.width);
|
||||
ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host));
|
||||
|
||||
ImGui::Text("Port:");
|
||||
ImGui::Text("%s", TR("rpc_port"));
|
||||
ImGui::SameLine(connLbl.position);
|
||||
ImGui::SetNextItemWidth(portInput.width);
|
||||
ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Username:");
|
||||
ImGui::Text("%s", TR("rpc_user"));
|
||||
ImGui::SameLine(connLbl.position);
|
||||
ImGui::SetNextItemWidth(cmb.width);
|
||||
ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user));
|
||||
|
||||
ImGui::Text("Password:");
|
||||
ImGui::Text("%s", TR("rpc_pass"));
|
||||
ImGui::SameLine(connLbl.position);
|
||||
ImGui::SetNextItemWidth(cmb.width);
|
||||
ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password),
|
||||
@@ -415,11 +423,11 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextDisabled("Note: Connection settings are usually auto-detected from DRAGONX.conf");
|
||||
ImGui::TextDisabled("%s", TR("settings_rpc_note"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (material::StyledButton("Test Connection", ImVec2(0,0), S.resolveFont("button"))) {
|
||||
if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) {
|
||||
if (app->rpc()) {
|
||||
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
||||
if (error.empty()) {
|
||||
@@ -439,15 +447,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
}
|
||||
|
||||
// Wallet tab
|
||||
if (ImGui::BeginTabItem("Wallet")) {
|
||||
if (ImGui::BeginTabItem(TR("wallet"))) {
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Wallet Maintenance");
|
||||
ImGui::Text("%s", TR("settings_wallet_maintenance"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (material::StyledButton("Rescan Blockchain", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
|
||||
if (material::StyledButton(TR("rescan"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
|
||||
if (app->rpc()) {
|
||||
// Start rescan from block 0
|
||||
app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) {
|
||||
@@ -465,45 +473,41 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
Notifications::instance().error("RPC client not initialized");
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled(" Rescan blockchain for missing transactions");
|
||||
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
static bool s_confirm_clear_ztx = false;
|
||||
if (material::StyledButton("Clear Saved Z-Transaction History", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
|
||||
if (material::StyledButton(TR("settings_clear_ztx_long"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
|
||||
s_confirm_clear_ztx = true;
|
||||
}
|
||||
ImGui::TextDisabled(" Delete locally stored shielded transaction data");
|
||||
ImGui::TextDisabled(" %s", TR("settings_clear_ztx_desc"));
|
||||
|
||||
// Confirmation dialog
|
||||
if (s_confirm_clear_ztx) {
|
||||
if (material::BeginOverlayDialog("Confirm Clear Z-Tx History", &s_confirm_clear_ztx, 480.0f, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) {
|
||||
ImGui::PushFont(material::Type().iconLarge());
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Warning");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"Clearing z-transaction history may cause your shielded balance to show as 0 "
|
||||
"until a wallet rescan is performed.");
|
||||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"If this happens, you will need to re-import your z-address private keys with "
|
||||
"rescan enabled to recover your balance.");
|
||||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
|
||||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
s_confirm_clear_ztx = 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("Clear Anyway", ImVec2(btnW, 40))) {
|
||||
if (ImGui::Button(TrId("clear_anyway", "clear_ztx_w").c_str(), ImVec2(btnW, 40))) {
|
||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||
if (util::Platform::deleteFile(ztx_file)) {
|
||||
Notifications::instance().success("Z-transaction history cleared");
|
||||
@@ -521,7 +525,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Wallet Info");
|
||||
ImGui::Text("%s", TR("settings_wallet_info"));
|
||||
ImGui::Spacing();
|
||||
|
||||
// Get actual wallet size
|
||||
@@ -529,35 +533,35 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
|
||||
if (wallet_size > 0) {
|
||||
std::string size_str = util::Platform::formatFileSize(wallet_size);
|
||||
ImGui::Text("Wallet file size: %s", size_str.c_str());
|
||||
ImGui::Text(TR("settings_wallet_file_size"), size_str.c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("Wallet file not found");
|
||||
ImGui::TextDisabled("%s", TR("settings_wallet_not_found"));
|
||||
}
|
||||
ImGui::Text("Wallet location: %s", wallet_path.c_str());
|
||||
ImGui::Text(TR("settings_wallet_location"), wallet_path.c_str());
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// Explorer tab
|
||||
if (ImGui::BeginTabItem("Explorer")) {
|
||||
if (ImGui::BeginTabItem(TR("explorer"))) {
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Block Explorer URLs");
|
||||
ImGui::TextDisabled("Configure external block explorer links");
|
||||
ImGui::Text("%s", TR("settings_block_explorer_urls"));
|
||||
ImGui::TextDisabled("%s", TR("settings_configure_explorer"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Transaction URL:");
|
||||
ImGui::Text("%s", TR("transaction_url"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer));
|
||||
|
||||
ImGui::Text("Address URL:");
|
||||
ImGui::Text("%s", TR("address_url"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("URLs should include a trailing slash. The txid/address will be appended.");
|
||||
ImGui::TextDisabled("%s", TR("settings_explorer_hint"));
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
@@ -570,13 +574,13 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Save/Cancel buttons
|
||||
if (material::StyledButton("Save", ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
||||
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
||||
saveSettingsFromUI(app->settings());
|
||||
Notifications::instance().success("Settings saved");
|
||||
*p_open = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
|
||||
// Reload settings to revert changes
|
||||
loadSettingsToUI(app->settings());
|
||||
// Revert skin to what was active when settings opened
|
||||
|
||||
@@ -76,24 +76,17 @@ void ShieldDialog::render(App* app)
|
||||
auto cancelBtn = S.button("dialogs.shield", "cancel-button");
|
||||
|
||||
const char* title = (s_mode == Mode::ShieldCoinbase)
|
||||
? "Shield Coinbase Rewards"
|
||||
: "Merge to Address";
|
||||
? TR("shield_title")
|
||||
: TR("merge_title");
|
||||
|
||||
if (material::BeginOverlayDialog(title, &s_open, win.width, 0.94f)) {
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
// Description
|
||||
if (s_mode == Mode::ShieldCoinbase) {
|
||||
ImGui::TextWrapped(
|
||||
"Shield your mining rewards by sending coinbase outputs from "
|
||||
"transparent addresses to a shielded address. This improves "
|
||||
"privacy by hiding your mining income."
|
||||
);
|
||||
ImGui::TextWrapped("%s", TR("shield_description"));
|
||||
} else {
|
||||
ImGui::TextWrapped(
|
||||
"Merge multiple UTXOs into a single shielded address. This can "
|
||||
"help reduce wallet size and improve privacy."
|
||||
);
|
||||
ImGui::TextWrapped("%s", TR("merge_description"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -102,18 +95,18 @@ void ShieldDialog::render(App* app)
|
||||
|
||||
// From address (for shield coinbase)
|
||||
if (s_mode == Mode::ShieldCoinbase) {
|
||||
ImGui::Text("From Address:");
|
||||
ImGui::Text("%s", TR("shield_from_address"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##FromAddr", s_from_address, sizeof(s_from_address));
|
||||
ImGui::TextDisabled("Use '*' to shield from all transparent addresses");
|
||||
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// To address (z-address dropdown)
|
||||
ImGui::Text("To Address (Shielded):");
|
||||
ImGui::Text("%s", TR("shield_to_address"));
|
||||
|
||||
// Get z-addresses for dropdown
|
||||
std::string to_display = s_to_address[0] ? s_to_address : "Select z-address...";
|
||||
std::string to_display = s_to_address[0] ? s_to_address : TR("shield_select_z");
|
||||
if (to_display.length() > static_cast<size_t>(addrLbl.truncate)) {
|
||||
to_display = to_display.substr(0, addrFrontLbl.truncate) + "..." + to_display.substr(to_display.length() - addrBackLbl.truncate);
|
||||
}
|
||||
@@ -142,7 +135,7 @@ void ShieldDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Fee
|
||||
ImGui::Text("Fee:");
|
||||
ImGui::Text("%s", TR("fee_label"));
|
||||
ImGui::SetNextItemWidth(feeInput.width);
|
||||
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
|
||||
ImGui::SameLine();
|
||||
@@ -151,11 +144,11 @@ void ShieldDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// UTXO limit
|
||||
ImGui::Text("UTXO Limit:");
|
||||
ImGui::Text("%s", TR("shield_utxo_limit"));
|
||||
ImGui::SetNextItemWidth(utxoInput.width);
|
||||
ImGui::InputInt("##Limit", &s_utxo_limit);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Max UTXOs per operation");
|
||||
ImGui::TextDisabled("%s", TR("shield_max_utxos"));
|
||||
if (s_utxo_limit < 1) s_utxo_limit = 1;
|
||||
if (s_utxo_limit > 100) s_utxo_limit = 100;
|
||||
|
||||
@@ -178,7 +171,7 @@ void ShieldDialog::render(App* app)
|
||||
|
||||
if (!can_submit) ImGui::BeginDisabled();
|
||||
|
||||
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? "Shield Funds" : "Merge Funds";
|
||||
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
|
||||
if (material::StyledButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
|
||||
s_operation_pending = true;
|
||||
s_status_message = "Submitting operation...";
|
||||
@@ -201,7 +194,7 @@ void ShieldDialog::render(App* app)
|
||||
if (error.empty()) {
|
||||
s_operation_id = result.value("opid", "");
|
||||
s_status_message = "Operation submitted: " + s_operation_id;
|
||||
Notifications::instance().success("Shield operation started");
|
||||
Notifications::instance().success(TR("shield_started"));
|
||||
} else {
|
||||
s_status_message = "Error: " + error;
|
||||
Notifications::instance().error("Shield failed: " + error);
|
||||
@@ -231,7 +224,7 @@ void ShieldDialog::render(App* app)
|
||||
if (error.empty()) {
|
||||
s_operation_id = result.value("opid", "");
|
||||
s_status_message = "Operation submitted: " + s_operation_id;
|
||||
Notifications::instance().success("Merge operation started");
|
||||
Notifications::instance().success(TR("merge_started"));
|
||||
} else {
|
||||
s_status_message = "Error: " + error;
|
||||
Notifications::instance().error("Merge failed: " + error);
|
||||
@@ -246,7 +239,7 @@ void ShieldDialog::render(App* app)
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
@@ -256,9 +249,9 @@ void ShieldDialog::render(App* app)
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Operation ID: %s", s_operation_id.c_str());
|
||||
ImGui::Text(TR("shield_operation_id"), s_operation_id.c_str());
|
||||
|
||||
if (material::StyledButton("Check Status", ImVec2(0,0), S.resolveFont(shieldBtn.font))) {
|
||||
if (material::StyledButton(TR("shield_check_status"), ImVec2(0,0), S.resolveFont(shieldBtn.font))) {
|
||||
std::string opid = s_operation_id;
|
||||
if (app->worker()) {
|
||||
app->worker()->post([rpc = app->rpc(), opid]() -> rpc::RPCWorker::MainCb {
|
||||
@@ -276,14 +269,14 @@ void ShieldDialog::render(App* app)
|
||||
auto& op = result[0];
|
||||
std::string status = op.value("status", "unknown");
|
||||
if (status == "success") {
|
||||
s_status_message = "Operation completed successfully!";
|
||||
Notifications::instance().success("Shield/merge completed!");
|
||||
s_status_message = TR("shield_completed");
|
||||
Notifications::instance().success(TR("shield_merge_done"));
|
||||
} else if (status == "failed") {
|
||||
std::string errMsg = op.value("error", nlohmann::json{}).value("message", "Unknown error");
|
||||
s_status_message = "Operation failed: " + errMsg;
|
||||
Notifications::instance().error("Operation failed: " + errMsg);
|
||||
} else if (status == "executing") {
|
||||
s_status_message = "Operation in progress...";
|
||||
s_status_message = TR("shield_in_progress");
|
||||
} else {
|
||||
s_status_message = "Status: " + status;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
auto memoInput = S.input("dialogs.transaction-details", "memo-input");
|
||||
auto bottomBtn = S.button("dialogs.transaction-details", "bottom-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Transaction Details", &s_open, win.width, 0.94f)) {
|
||||
if (material::BeginOverlayDialog(TR("tx_details_title"), &s_open, win.width, 0.94f)) {
|
||||
const auto& tx = s_transaction;
|
||||
|
||||
// Type indicator with color
|
||||
@@ -52,16 +52,16 @@ void TransactionDetailsDialog::render(App* app)
|
||||
std::string type_display;
|
||||
if (tx.type == "receive") {
|
||||
type_color = ImVec4(0.3f, 0.8f, 0.3f, 1.0f);
|
||||
type_display = "RECEIVED";
|
||||
type_display = TR("tx_received");
|
||||
} else if (tx.type == "send") {
|
||||
type_color = ImVec4(0.8f, 0.3f, 0.3f, 1.0f);
|
||||
type_display = "SENT";
|
||||
type_display = TR("tx_sent");
|
||||
} else if (tx.type == "generate" || tx.type == "mined") {
|
||||
type_color = ImVec4(0.3f, 0.6f, 0.9f, 1.0f);
|
||||
type_display = "MINED";
|
||||
type_display = TR("tx_mined");
|
||||
} else if (tx.type == "immature") {
|
||||
type_color = ImVec4(0.8f, 0.8f, 0.3f, 1.0f);
|
||||
type_display = "IMMATURE";
|
||||
type_display = TR("tx_immature");
|
||||
} else {
|
||||
type_color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
type_display = tx.type;
|
||||
@@ -72,11 +72,11 @@ void TransactionDetailsDialog::render(App* app)
|
||||
|
||||
// Confirmations badge
|
||||
if (tx.confirmations == 0) {
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Pending");
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "%s", TR("pending"));
|
||||
} else if (tx.confirmations < 10) {
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), "%d confirmations", tx.confirmations);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), TR("tx_confirmations"), tx.confirmations);
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%d confirmations", tx.confirmations);
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), TR("tx_confirmations"), tx.confirmations);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -84,7 +84,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Amount (prominent display)
|
||||
ImGui::Text("Amount:");
|
||||
ImGui::Text("%s", TR("amount_label"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (tx.amount >= 0) {
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "+%.8f DRGX", tx.amount);
|
||||
@@ -102,7 +102,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Date/Time
|
||||
ImGui::Text("Date:");
|
||||
ImGui::Text("%s", TR("date_label"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::Text("%s", tx.getTimeString().c_str());
|
||||
|
||||
@@ -111,7 +111,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Transaction ID
|
||||
ImGui::Text("Transaction ID:");
|
||||
ImGui::Text("%s", TR("tx_id_label"));
|
||||
char txid_buf[128];
|
||||
strncpy(txid_buf, tx.txid.c_str(), sizeof(txid_buf) - 1);
|
||||
txid_buf[sizeof(txid_buf) - 1] = '\0';
|
||||
@@ -126,7 +126,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
|
||||
// Address
|
||||
if (!tx.address.empty()) {
|
||||
ImGui::Text(tx.type == "send" ? "To Address:" : "From Address:");
|
||||
ImGui::Text("%s", tx.type == "send" ? TR("tx_to_address") : TR("tx_from_address"));
|
||||
|
||||
// Use multiline for z-addresses
|
||||
if (tx.address.length() > 50) {
|
||||
@@ -155,7 +155,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Memo:");
|
||||
ImGui::Text("%s", TR("memo_label"));
|
||||
char memo_buf[512];
|
||||
strncpy(memo_buf, tx.memo.c_str(), sizeof(memo_buf) - 1);
|
||||
memo_buf[sizeof(memo_buf) - 1] = '\0';
|
||||
@@ -173,7 +173,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f;
|
||||
ImGui::SetCursorPosX(start_x);
|
||||
|
||||
if (material::StyledButton("View on Explorer", ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
|
||||
// Platform-specific URL opening
|
||||
#ifdef _WIN32
|
||||
@@ -189,7 +189,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "transaction_details_dialog.h"
|
||||
#include "export_transactions_dialog.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../theme.h"
|
||||
@@ -28,6 +29,10 @@ namespace ui {
|
||||
|
||||
using namespace material;
|
||||
|
||||
static std::string TrId(const char* key, const char* id) {
|
||||
return std::string(TR(key)) + "##" + id;
|
||||
}
|
||||
|
||||
// Helper to truncate strings
|
||||
static std::string truncateString(const std::string& str, int maxLen = 16) {
|
||||
if (str.length() <= static_cast<size_t>(maxLen)) return str;
|
||||
@@ -66,7 +71,7 @@ struct DisplayTx {
|
||||
};
|
||||
|
||||
std::string DisplayTx::getTimeString() const {
|
||||
if (timestamp <= 0) return "Pending";
|
||||
if (timestamp <= 0) return TR("pending");
|
||||
std::time_t t = static_cast<std::time_t>(timestamp);
|
||||
char buf[64];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", std::localtime(&t));
|
||||
@@ -79,10 +84,11 @@ static std::string timeAgo(int64_t timestamp) {
|
||||
int64_t now = (int64_t)std::time(nullptr);
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60) return std::to_string(diff) + "s ago";
|
||||
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
|
||||
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
|
||||
return std::to_string(diff / 86400) + "d ago";
|
||||
char buf[32];
|
||||
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
|
||||
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
|
||||
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
|
||||
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
|
||||
}
|
||||
|
||||
// Draw a small transaction-type icon
|
||||
@@ -191,10 +197,10 @@ void RenderTransactionsTab(App* app)
|
||||
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
|
||||
|
||||
float labelX = cx + iconSz * 3.0f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "RECEIVED");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("received_upper"));
|
||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d txs", recvCount);
|
||||
snprintf(buf, sizeof(buf), TR("txs_count"), recvCount);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||
cy += capFont->LegacySize + Layout::spacingXs();
|
||||
|
||||
@@ -221,10 +227,10 @@ void RenderTransactionsTab(App* app)
|
||||
DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);
|
||||
|
||||
float labelX = cx + iconSz * 3.0f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "SENT");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("sent_upper"));
|
||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d txs", sendCount);
|
||||
snprintf(buf, sizeof(buf), TR("txs_count"), sendCount);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||
cy += capFont->LegacySize + Layout::spacingXs();
|
||||
|
||||
@@ -251,10 +257,10 @@ void RenderTransactionsTab(App* app)
|
||||
DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);
|
||||
|
||||
float labelX = cx + iconSz * 3.0f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "MINED");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("mined_upper"));
|
||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d txs", minedCount);
|
||||
snprintf(buf, sizeof(buf), TR("txs_count"), minedCount);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||
cy += capFont->LegacySize + Layout::spacingXs();
|
||||
|
||||
@@ -292,23 +298,23 @@ void RenderTransactionsTab(App* app)
|
||||
|
||||
ImGui::SetNextItemWidth(searchWidth);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
|
||||
ImGui::InputTextWithHint("##TxSearch", "Search...", search_filter, sizeof(search_filter));
|
||||
ImGui::InputTextWithHint("##TxSearch", TR("search_placeholder"), search_filter, sizeof(search_filter));
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
float filterGap = std::max(8.0f, ((filterGapEl.size > 0) ? filterGapEl.size : 20.0f) * hs);
|
||||
ImGui::SameLine(0, filterGap);
|
||||
float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs);
|
||||
ImGui::SetNextItemWidth(comboW);
|
||||
const char* types[] = { "All", "Sent", "Received", "Mined" };
|
||||
const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") };
|
||||
ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));
|
||||
|
||||
ImGui::SameLine(0, filterGap);
|
||||
if (TactileButton("Refresh", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("refresh", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
app->refreshNow();
|
||||
}
|
||||
|
||||
ImGui::SameLine(0, filterGap);
|
||||
if (TactileButton("Export CSV", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TrId("export_csv", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
ExportTransactionsDialog::show();
|
||||
}
|
||||
|
||||
@@ -442,7 +448,7 @@ void RenderTransactionsTab(App* app)
|
||||
|
||||
// ---- Heading line: "TRANSACTIONS" left, pagination right ----
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTIONS");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transactions_upper"));
|
||||
|
||||
if (totalPages > 1) {
|
||||
float paginationH = ImGui::GetFrameHeight();
|
||||
@@ -557,13 +563,13 @@ void RenderTransactionsTab(App* app)
|
||||
{
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
|
||||
} else if (state.transactions.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No transactions found");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
|
||||
} else if (filtered_indices.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No matching transactions");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
|
||||
} else {
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
|
||||
float innerW = ImGui::GetContentRegionAvail().x;
|
||||
@@ -592,15 +598,15 @@ void RenderTransactionsTab(App* app)
|
||||
ImU32 iconCol;
|
||||
const char* typeStr;
|
||||
if (tx.display_type == "shield") {
|
||||
iconCol = Primary(); typeStr = "Shielded";
|
||||
iconCol = Primary(); typeStr = TR("shielded_type");
|
||||
} else if (tx.display_type == "receive") {
|
||||
iconCol = greenCol; typeStr = "Recv";
|
||||
iconCol = greenCol; typeStr = TR("recv_type");
|
||||
} else if (tx.display_type == "send") {
|
||||
iconCol = redCol; typeStr = "Sent";
|
||||
iconCol = redCol; typeStr = TR("sent_type");
|
||||
} else if (tx.display_type == "immature") {
|
||||
iconCol = Warning(); typeStr = "Immature";
|
||||
iconCol = Warning(); typeStr = TR("immature_type");
|
||||
} else {
|
||||
iconCol = goldCol; typeStr = "Mined";
|
||||
iconCol = goldCol; typeStr = TR("mined_type");
|
||||
}
|
||||
|
||||
// Expanded selection accent
|
||||
@@ -669,14 +675,14 @@ void RenderTransactionsTab(App* app)
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
statusStr = "Pending"; statusCol = Warning();
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < 10) {
|
||||
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
statusStr = buf; statusCol = Warning();
|
||||
} else if (tx.confirmations >= 100 && (tx.display_type == "generate" || tx.display_type == "mined")) {
|
||||
statusStr = "Mature"; statusCol = greenCol;
|
||||
statusStr = TR("mature"); statusCol = greenCol;
|
||||
} else {
|
||||
statusStr = "Confirmed"; statusCol = WithAlpha(Success(), 140);
|
||||
statusStr = TR("confirmed"); statusCol = WithAlpha(Success(), 140);
|
||||
}
|
||||
// Position status badge in the middle-right area
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
@@ -707,14 +713,14 @@ void RenderTransactionsTab(App* app)
|
||||
// Context menu
|
||||
const auto& acrylicTheme = GetCurrentAcrylicTheme();
|
||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("TxContext", 0, acrylicTheme.menu)) {
|
||||
if (ImGui::MenuItem("Copy Address") && !tx.address.empty()) {
|
||||
if (ImGui::MenuItem(TR("copy_address")) && !tx.address.empty()) {
|
||||
ImGui::SetClipboardText(tx.address.c_str());
|
||||
}
|
||||
if (ImGui::MenuItem("Copy TxID")) {
|
||||
if (ImGui::MenuItem(TR("copy_txid"))) {
|
||||
ImGui::SetClipboardText(tx.txid.c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("View on Explorer")) {
|
||||
if (ImGui::MenuItem(TR("view_on_explorer"))) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
|
||||
#ifdef _WIN32
|
||||
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
@@ -726,7 +732,7 @@ void RenderTransactionsTab(App* app)
|
||||
system(cmd.c_str());
|
||||
#endif
|
||||
}
|
||||
if (ImGui::MenuItem("View Details")) {
|
||||
if (ImGui::MenuItem(TR("view_details"))) {
|
||||
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
|
||||
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
|
||||
}
|
||||
@@ -747,19 +753,19 @@ void RenderTransactionsTab(App* app)
|
||||
|
||||
// From address
|
||||
if (!tx.from_address.empty()) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "FROM");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
|
||||
ImGui::TextWrapped("%s", tx.from_address.c_str());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
// To address
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
|
||||
tx.display_type == "send" ? "TO" : (tx.display_type == "shield" ? "SHIELDED TO" : "ADDRESS"));
|
||||
tx.display_type == "send" ? TR("to_upper") : (tx.display_type == "shield" ? TR("shielded_to") : TR("address_upper")));
|
||||
ImGui::TextWrapped("%s", tx.address.c_str());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// TxID (full, copyable)
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTION ID");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transaction_id"));
|
||||
ImGui::TextWrapped("%s", tx.txid.c_str());
|
||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsItemClicked()) ImGui::SetClipboardText(tx.txid.c_str());
|
||||
@@ -767,13 +773,13 @@ void RenderTransactionsTab(App* app)
|
||||
|
||||
// Memo
|
||||
if (!tx.memo.empty()) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
|
||||
ImGui::TextWrapped("%s", tx.memo.c_str());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
// Confirmations + time
|
||||
snprintf(buf, sizeof(buf), "%d confirmations | %s",
|
||||
snprintf(buf, sizeof(buf), TR("confirmations_display"),
|
||||
tx.confirmations, tx.getTimeString().c_str());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
@@ -782,15 +788,15 @@ void RenderTransactionsTab(App* app)
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30)));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.transactions", "detail-btn-rounding").size);
|
||||
if (TactileSmallButton("Copy TxID##detail", S.resolveFont("button"))) {
|
||||
if (TactileSmallButton(TrId("copy_txid", "detail").c_str(), S.resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(tx.txid.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (!tx.address.empty() && TactileSmallButton("Copy Address##detail", S.resolveFont("button"))) {
|
||||
if (!tx.address.empty() && TactileSmallButton(TrId("copy_address", "detail").c_str(), S.resolveFont("button"))) {
|
||||
ImGui::SetClipboardText(tx.address.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton("Explorer##detail", S.resolveFont("button"))) {
|
||||
if (TactileSmallButton(TrId("explorer", "detail").c_str(), S.resolveFont("button"))) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
|
||||
#ifdef _WIN32
|
||||
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
@@ -803,7 +809,7 @@ void RenderTransactionsTab(App* app)
|
||||
#endif
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton("Full Details##detail", S.resolveFont("button"))) {
|
||||
if (TactileSmallButton(TrId("full_details", "detail").c_str(), S.resolveFont("button"))) {
|
||||
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
|
||||
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
|
||||
}
|
||||
@@ -849,7 +855,7 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
|
||||
// Status line with page info
|
||||
snprintf(buf, sizeof(buf), "Showing %d\xe2\x80\x93%d of %d transactions (total: %zu)",
|
||||
snprintf(buf, sizeof(buf), TR("showing_transactions"),
|
||||
filtered_count > 0 ? pageStart + 1 : 0, pageEnd, filtered_count, state.transactions.size());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "../../app.h"
|
||||
#include "../../rpc/rpc_client.h"
|
||||
#include "../../rpc/rpc_worker.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../theme.h"
|
||||
@@ -52,15 +53,15 @@ void ValidateAddressDialog::render(App* app)
|
||||
auto lbl = S.label("dialogs.validate-address", "label");
|
||||
auto closeBtn = S.button("dialogs.validate-address", "close-button");
|
||||
|
||||
if (material::BeginOverlayDialog("Validate Address", &s_open, win.width, 0.94f)) {
|
||||
ImGui::TextWrapped("Enter a DragonX address to check if it's valid and whether it belongs to this wallet.");
|
||||
if (material::BeginOverlayDialog(TR("validate_title"), &s_open, win.width, 0.94f)) {
|
||||
ImGui::TextWrapped("%s", TR("validate_description"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Address input
|
||||
ImGui::Text("Address:");
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
bool enter_pressed = ImGui::InputText("##ValidateAddr", s_address_input, sizeof(s_address_input),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
@@ -74,7 +75,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton("Validate", ImVec2(valBtn.width, 0), S.resolveFont(valBtn.font)) || (enter_pressed && can_validate)) {
|
||||
if (material::StyledButton(TR("validate_btn"), ImVec2(valBtn.width, 0), S.resolveFont(valBtn.font)) || (enter_pressed && can_validate)) {
|
||||
s_validating = true;
|
||||
s_validated = false;
|
||||
s_error_message.clear();
|
||||
@@ -100,7 +101,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
if (error.empty()) {
|
||||
s_is_valid = valid;
|
||||
s_is_mine = mine;
|
||||
s_address_type = "Shielded (z-address)";
|
||||
s_address_type = TR("validate_shielded_type");
|
||||
} else {
|
||||
s_error_message = error;
|
||||
s_is_valid = false;
|
||||
@@ -126,7 +127,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
if (error.empty()) {
|
||||
s_is_valid = valid;
|
||||
s_is_mine = mine;
|
||||
s_address_type = "Transparent (t-address)";
|
||||
s_address_type = TR("validate_transparent_type");
|
||||
} else {
|
||||
s_error_message = error;
|
||||
s_is_valid = false;
|
||||
@@ -145,7 +146,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton("Paste", ImVec2(pasteBtn.width, 0), S.resolveFont(pasteBtn.font))) {
|
||||
if (material::StyledButton(TR("paste"), ImVec2(pasteBtn.width, 0), S.resolveFont(pasteBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) {
|
||||
strncpy(s_address_input, clipboard, sizeof(s_address_input) - 1);
|
||||
@@ -156,7 +157,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
|
||||
if (s_validating) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Validating...");
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("validating"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -165,39 +166,39 @@ void ValidateAddressDialog::render(App* app)
|
||||
|
||||
// Results
|
||||
if (s_validated) {
|
||||
ImGui::Text("Results:");
|
||||
ImGui::Text("%s", TR("validate_results"));
|
||||
ImGui::Spacing();
|
||||
|
||||
if (!s_error_message.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Error: %s", s_error_message.c_str());
|
||||
} else {
|
||||
// Valid/Invalid indicator
|
||||
ImGui::Text("Status:");
|
||||
ImGui::Text("%s", TR("validate_status"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (s_is_valid) {
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "VALID");
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_valid"));
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "INVALID");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("validate_invalid"));
|
||||
}
|
||||
|
||||
if (s_is_valid) {
|
||||
// Address type
|
||||
ImGui::Text("Type:");
|
||||
ImGui::Text("%s", TR("validate_type"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
ImGui::Text("%s", s_address_type.c_str());
|
||||
|
||||
// Is mine?
|
||||
ImGui::Text("Ownership:");
|
||||
ImGui::Text("%s", TR("validate_ownership"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (s_is_mine) {
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "This wallet owns this address");
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_is_mine"));
|
||||
} else {
|
||||
ImGui::TextDisabled("Not owned by this wallet");
|
||||
ImGui::TextDisabled("%s", TR("validate_not_mine"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!app->isConnected()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Not connected to daemon");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("not_connected"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -205,7 +206,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
// Close button at bottom
|
||||
float button_width = closeBtn.width;
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) / 2.0f);
|
||||
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "i18n.h"
|
||||
#include "platform.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
@@ -28,15 +29,17 @@ using json = nlohmann::json;
|
||||
I18n::I18n()
|
||||
{
|
||||
// Register built-in languages
|
||||
// CJK glyphs are provided by the merged NotoSansCJK-Subset fallback font.
|
||||
// Cyrillic glyphs are included in the Ubuntu font glyph ranges.
|
||||
registerLanguage("en", "English");
|
||||
registerLanguage("es", "Español");
|
||||
registerLanguage("zh", "中文");
|
||||
registerLanguage("ru", "Русский");
|
||||
registerLanguage("zh", "Chinese");
|
||||
registerLanguage("ru", "Russian");
|
||||
registerLanguage("de", "Deutsch");
|
||||
registerLanguage("fr", "Français");
|
||||
registerLanguage("pt", "Português");
|
||||
registerLanguage("ja", "日本語");
|
||||
registerLanguage("ko", "한국어");
|
||||
registerLanguage("ja", "Japanese");
|
||||
registerLanguage("ko", "Korean");
|
||||
|
||||
// Load default English strings (built-in fallback)
|
||||
loadLanguage("en");
|
||||
@@ -50,26 +53,35 @@ I18n& I18n::instance()
|
||||
|
||||
bool I18n::loadLanguage(const std::string& locale)
|
||||
{
|
||||
// First, try to load from file (allows user overrides)
|
||||
std::string lang_file = "res/lang/" + locale + ".json";
|
||||
std::ifstream file(lang_file);
|
||||
|
||||
if (file.is_open()) {
|
||||
// Try to load from file: CWD-relative first, then exe-relative
|
||||
std::string rel_path = "res/lang/" + locale + ".json";
|
||||
std::string exe_path = Platform::getExecutableDirectory() + "/" + rel_path;
|
||||
|
||||
// Try both paths; prefer whichever opens successfully
|
||||
for (const auto& lang_file : { rel_path, exe_path }) {
|
||||
std::ifstream file(lang_file);
|
||||
if (!file.is_open()) continue;
|
||||
|
||||
try {
|
||||
json j;
|
||||
file >> j;
|
||||
|
||||
strings_.clear();
|
||||
|
||||
// Load English built-in as fallback base for non-English
|
||||
if (locale != "en") {
|
||||
loadBuiltinEnglish();
|
||||
} else {
|
||||
strings_.clear();
|
||||
}
|
||||
for (auto& [key, value] : j.items()) {
|
||||
if (value.is_string()) {
|
||||
strings_[key] = value.get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
current_locale_ = locale;
|
||||
DEBUG_LOGF("Loaded language file: %s (%zu strings)\n", lang_file.c_str(), strings_.size());
|
||||
return true;
|
||||
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error parsing language file %s: %s\n", lang_file.c_str(), e.what());
|
||||
}
|
||||
@@ -110,7 +122,12 @@ bool I18n::loadLanguage(const std::string& locale)
|
||||
std::string json_str(reinterpret_cast<const char*>(embedded_data), embedded_size);
|
||||
json j = json::parse(json_str);
|
||||
|
||||
strings_.clear();
|
||||
// Load English built-in as fallback base for non-English
|
||||
if (locale != "en") {
|
||||
loadBuiltinEnglish();
|
||||
} else {
|
||||
strings_.clear();
|
||||
}
|
||||
for (auto& [key, value] : j.items()) {
|
||||
if (value.is_string()) {
|
||||
strings_[key] = value.get<std::string>();
|
||||
@@ -128,17 +145,323 @@ bool I18n::loadLanguage(const std::string& locale)
|
||||
|
||||
// If English, use built-in strings
|
||||
if (locale == "en") {
|
||||
loadBuiltinEnglish();
|
||||
current_locale_ = "en";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void I18n::loadBuiltinEnglish()
|
||||
{
|
||||
strings_.clear();
|
||||
|
||||
// Navigation & Tabs
|
||||
strings_["overview"] = "Overview";
|
||||
strings_["balance"] = "Balance";
|
||||
strings_["send"] = "Send";
|
||||
strings_["receive"] = "Receive";
|
||||
strings_["transactions"] = "Transactions";
|
||||
strings_["history"] = "History";
|
||||
strings_["mining"] = "Mining";
|
||||
strings_["peers"] = "Peers";
|
||||
strings_["market"] = "Market";
|
||||
strings_["settings"] = "Settings";
|
||||
strings_["console"] = "Console";
|
||||
strings_["tools"] = "TOOLS";
|
||||
strings_["advanced"] = "ADVANCED";
|
||||
|
||||
// Settings sections
|
||||
strings_["appearance"] = "APPEARANCE";
|
||||
strings_["wallet"] = "WALLET";
|
||||
strings_["node_security"] = "NODE & SECURITY";
|
||||
strings_["node"] = "NODE";
|
||||
strings_["security"] = "SECURITY";
|
||||
strings_["explorer"] = "EXPLORER";
|
||||
strings_["about"] = "About";
|
||||
strings_["backup_data"] = "BACKUP & DATA";
|
||||
strings_["balance_layout"] = "Balance Layout";
|
||||
strings_["low_spec_mode"] = "Low-spec mode";
|
||||
strings_["simple_background"] = "Simple background";
|
||||
strings_["console_scanline"] = "Console scanline";
|
||||
strings_["theme_effects"] = "Theme effects";
|
||||
strings_["acrylic"] = "Acrylic";
|
||||
strings_["noise"] = "Noise";
|
||||
strings_["ui_opacity"] = "UI Opacity";
|
||||
strings_["window_opacity"] = "Window Opacity";
|
||||
strings_["font_scale"] = "Font Scale";
|
||||
strings_["refresh"] = "Refresh";
|
||||
strings_["website"] = "Website";
|
||||
strings_["report_bug"] = "Report Bug";
|
||||
strings_["save_settings"] = "Save Settings";
|
||||
strings_["reset_to_defaults"] = "Reset to Defaults";
|
||||
|
||||
// Wallet settings
|
||||
strings_["save_z_transactions"] = "Save shielded tx history";
|
||||
strings_["auto_shield"] = "Auto-shield";
|
||||
strings_["use_tor"] = "Use Tor";
|
||||
strings_["keep_daemon"] = "Keep daemon running";
|
||||
strings_["stop_external"] = "Stop external daemon";
|
||||
strings_["verbose_logging"] = "Verbose logging";
|
||||
strings_["mine_when_idle"] = "Mine when idle";
|
||||
strings_["setup_wizard"] = "Run Setup Wizard...";
|
||||
|
||||
// RPC / Explorer settings
|
||||
strings_["rpc_host"] = "Host";
|
||||
strings_["rpc_port"] = "Port";
|
||||
strings_["rpc_user"] = "Username";
|
||||
strings_["rpc_pass"] = "Password";
|
||||
strings_["transaction_url"] = "Transaction URL";
|
||||
strings_["address_url"] = "Address URL";
|
||||
strings_["custom_fees"] = "Allow custom transaction fees";
|
||||
strings_["fetch_prices"] = "Fetch price data from CoinGecko";
|
||||
strings_["block_explorer"] = "Block Explorer";
|
||||
strings_["test_connection"] = "Test Connection";
|
||||
strings_["rescan"] = "Rescan Blockchain";
|
||||
|
||||
// Settings: buttons
|
||||
strings_["settings_address_book"] = "Address Book...";
|
||||
strings_["settings_validate_address"] = "Validate Address...";
|
||||
strings_["settings_request_payment"] = "Request Payment...";
|
||||
strings_["settings_shield_mining"] = "Shield Mining...";
|
||||
strings_["settings_merge_to_address"] = "Merge to Address...";
|
||||
strings_["settings_clear_ztx"] = "Clear Z-Tx History";
|
||||
strings_["settings_import_key"] = "Import Key...";
|
||||
strings_["settings_export_key"] = "Export Key...";
|
||||
strings_["settings_export_all"] = "Export All...";
|
||||
strings_["settings_backup"] = "Backup...";
|
||||
strings_["settings_export_csv"] = "Export CSV...";
|
||||
strings_["settings_encrypt_wallet"] = "Encrypt Wallet";
|
||||
strings_["settings_change_passphrase"] = "Change Passphrase";
|
||||
strings_["settings_lock_now"] = "Lock Now";
|
||||
strings_["settings_remove_encryption"] = "Remove Encryption";
|
||||
strings_["settings_set_pin"] = "Set PIN";
|
||||
strings_["settings_change_pin"] = "Change PIN";
|
||||
strings_["settings_remove_pin"] = "Remove PIN";
|
||||
strings_["settings_restart_daemon"] = "Restart daemon";
|
||||
strings_["clear_anyway"] = "Clear Anyway";
|
||||
|
||||
// Settings: labels / text
|
||||
strings_["settings_builtin"] = "Built-in";
|
||||
strings_["settings_custom"] = "Custom";
|
||||
strings_["settings_not_found"] = "Not found";
|
||||
strings_["settings_idle_after"] = "after";
|
||||
strings_["settings_not_encrypted"] = "Not encrypted";
|
||||
strings_["settings_locked"] = "Locked";
|
||||
strings_["settings_unlocked"] = "Unlocked";
|
||||
strings_["settings_quick_unlock_pin"] = "Quick-unlock PIN";
|
||||
strings_["settings_pin_active"] = "PIN";
|
||||
strings_["settings_encrypt_first_pin"] = "Encrypt wallet first to enable PIN";
|
||||
strings_["settings_data_dir"] = "Data Dir:";
|
||||
strings_["settings_wallet_size_label"] = "Wallet Size:";
|
||||
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
|
||||
strings_["settings_auto_detected"] = "Auto-detected from DRAGONX.conf";
|
||||
strings_["settings_visual_effects"] = "Visual Effects";
|
||||
strings_["settings_acrylic_level"] = "Acrylic Level:";
|
||||
strings_["settings_noise_opacity"] = "Noise Opacity:";
|
||||
strings_["settings_privacy"] = "Privacy";
|
||||
strings_["settings_other"] = "Other";
|
||||
strings_["settings_rpc_connection"] = "RPC Connection";
|
||||
strings_["settings_configure_rpc"] = "Configure connection to dragonxd daemon";
|
||||
strings_["settings_wallet_maintenance"] = "Wallet Maintenance";
|
||||
strings_["settings_wallet_info"] = "Wallet Info";
|
||||
strings_["settings_block_explorer_urls"] = "Block Explorer URLs";
|
||||
strings_["settings_configure_explorer"] = "Configure external block explorer links";
|
||||
strings_["settings_auto_lock"] = "AUTO-LOCK";
|
||||
strings_["settings_wallet_file_size"] = "Wallet file size: %s";
|
||||
strings_["settings_wallet_location"] = "Wallet location: %s";
|
||||
strings_["settings_rpc_note"] = "Note: Connection settings are usually auto-detected from DRAGONX.conf";
|
||||
strings_["settings_explorer_hint"] = "URLs should include a trailing slash. The txid/address will be appended.";
|
||||
strings_["settings_connection"] = "Connection";
|
||||
strings_["settings_reduce_transparency"] = "Reduce transparency";
|
||||
strings_["settings_save_shielded_local"] = "Save shielded transaction history locally";
|
||||
strings_["settings_auto_shield_funds"] = "Auto-shield transparent funds";
|
||||
strings_["settings_use_tor_network"] = "Use Tor for network connections";
|
||||
strings_["settings_gradient_bg"] = "Gradient bg";
|
||||
|
||||
// Settings: tooltips
|
||||
strings_["tt_theme_hotkey"] = "Hotkey: Ctrl+Left/Right to cycle themes";
|
||||
strings_["tt_layout_hotkey"] = "Hotkey: Left/Right arrow keys to cycle Balance layouts";
|
||||
strings_["tt_language"] = "Interface language for the wallet UI";
|
||||
strings_["tt_scan_themes"] = "Scan for new themes.\nPlace theme folders in:\n%s";
|
||||
strings_["tt_low_spec"] = "Disable all heavy visual effects\nHotkey: Ctrl+Shift+Down";
|
||||
strings_["tt_simple_bg"] = "Use a simple gradient for the background\nHotkey: Ctrl+Up";
|
||||
strings_["tt_simple_bg_alt"] = "Use a gradient version of the theme background image\nHotkey: Ctrl+Up";
|
||||
strings_["tt_scanline"] = "CRT scanline effect in console";
|
||||
strings_["tt_theme_effects"] = "Shimmer, glow, hue-cycling per theme";
|
||||
strings_["tt_blur"] = "Blur amount (0%% = off, 100%% = maximum)";
|
||||
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_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";
|
||||
strings_["tt_request_payment"] = "Generate a payment request with QR code";
|
||||
strings_["tt_shield_mining"] = "Move transparent mining rewards to a shielded address";
|
||||
strings_["tt_merge"] = "Consolidate multiple UTXOs into one address";
|
||||
strings_["tt_clear_ztx"] = "Delete locally cached z-transaction history";
|
||||
strings_["tt_save_ztx"] = "Store z-address transaction history locally for faster loading";
|
||||
strings_["tt_auto_shield"] = "Automatically move transparent balance to shielded addresses for privacy";
|
||||
strings_["tt_tor"] = "Route daemon connections through the Tor network for anonymity";
|
||||
strings_["tt_keep_daemon"] = "Daemon will still stop when running the setup wizard";
|
||||
strings_["tt_stop_external"] = "Applies when connecting to a daemon\nyou started outside this wallet";
|
||||
strings_["tt_verbose"] = "Log detailed connection diagnostics,\ndaemon state, and port owner info\nto the Console tab";
|
||||
strings_["tt_mine_idle"] = "Automatically start mining when the\nsystem is idle (no keyboard/mouse input)";
|
||||
strings_["tt_idle_delay"] = "How long to wait before starting mining";
|
||||
strings_["tt_wizard"] = "Re-run the initial setup wizard\nDaemon will be restarted";
|
||||
strings_["tt_open_dir"] = "Click to open in file explorer";
|
||||
strings_["tt_rpc_host"] = "Hostname of the DragonX daemon";
|
||||
strings_["tt_rpc_user"] = "RPC authentication username";
|
||||
strings_["tt_rpc_port"] = "Port for daemon RPC connections";
|
||||
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_encrypt"] = "Encrypt wallet.dat with a passphrase";
|
||||
strings_["tt_change_pass"] = "Change the wallet encryption passphrase";
|
||||
strings_["tt_lock"] = "Lock the wallet immediately";
|
||||
strings_["tt_remove_encrypt"] = "Remove encryption and store wallet unprotected";
|
||||
strings_["tt_auto_lock"] = "Lock wallet after this much inactivity";
|
||||
strings_["tt_set_pin"] = "Set a 4-8 digit PIN for quick unlock";
|
||||
strings_["tt_change_pin"] = "Change your unlock PIN";
|
||||
strings_["tt_remove_pin"] = "Remove PIN and require passphrase to unlock";
|
||||
strings_["tt_tx_url"] = "Base URL for viewing transactions in a block explorer";
|
||||
strings_["tt_addr_url"] = "Base URL for viewing addresses in a block explorer";
|
||||
strings_["tt_custom_fees"] = "Enable manual fee entry when sending transactions";
|
||||
strings_["tt_fetch_prices"] = "Retrieve DRGX market prices from CoinGecko API";
|
||||
strings_["tt_block_explorer"] = "Open the DragonX block explorer in your browser";
|
||||
strings_["tt_website"] = "Open the DragonX website";
|
||||
strings_["tt_report_bug"] = "Report an issue on the project tracker";
|
||||
strings_["tt_save_settings"] = "Save all settings to disk";
|
||||
strings_["tt_reset_settings"] = "Reload settings from disk (undo unsaved changes)";
|
||||
strings_["tt_debug_collapse"] = "Collapse debug logging options";
|
||||
strings_["tt_debug_expand"] = "Expand debug logging options";
|
||||
strings_["tt_restart_daemon"] = "Restart the daemon to apply debug logging changes";
|
||||
|
||||
// Settings: additional tooltips (keys/data row)
|
||||
strings_["tt_import_key"] = "Import a private key (zkey or tkey) into this wallet";
|
||||
strings_["tt_export_key"] = "Export the private key for the selected address";
|
||||
strings_["tt_export_all"] = "Export all private keys to a file";
|
||||
strings_["tt_backup"] = "Create a backup of your wallet.dat file";
|
||||
strings_["tt_export_csv"] = "Export transaction history as a CSV spreadsheet";
|
||||
|
||||
// Settings: about / debug
|
||||
strings_["settings_about_text"] = "A shielded cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui for a lightweight, portable experience.";
|
||||
strings_["settings_copyright"] = "Copyright 2024-2026 DragonX Developers | GPLv3 License";
|
||||
strings_["debug_logging"] = "DEBUG LOGGING";
|
||||
strings_["settings_debug_select"] = "Select categories to enable daemon debug logging (-debug= flags).";
|
||||
strings_["settings_debug_restart_note"] = "Changes take effect after restarting the daemon.";
|
||||
|
||||
// Settings window (legacy dialog) descriptions
|
||||
strings_["settings_language_note"] = "Note: Some text requires restart to update";
|
||||
strings_["settings_solid_colors_desc"] = "Use solid colors instead of blur effects (accessibility)";
|
||||
strings_["settings_gradient_desc"] = "Replace textured backgrounds with smooth gradients";
|
||||
strings_["settings_save_shielded_desc"] = "Stores z-addr transactions in a local file for viewing";
|
||||
strings_["settings_auto_shield_desc"] = "Automatically move transparent funds to shielded addresses";
|
||||
strings_["settings_tor_desc"] = "Route all connections through Tor for enhanced privacy";
|
||||
strings_["settings_rescan_desc"] = "Rescan blockchain for missing transactions";
|
||||
strings_["settings_clear_ztx_long"] = "Clear Saved Z-Transaction History";
|
||||
strings_["settings_clear_ztx_desc"] = "Delete locally stored shielded transaction data";
|
||||
strings_["settings_wallet_not_found"] = "Wallet file not found";
|
||||
|
||||
// Balance tab: address actions
|
||||
strings_["hide_zero_balances"] = "Hide 0 Balances";
|
||||
strings_["restore_address"] = "Restore Address";
|
||||
strings_["hide_address"] = "Hide Address";
|
||||
strings_["remove_favorite"] = "Remove Favorite";
|
||||
strings_["favorite_address"] = "Favorite Address";
|
||||
strings_["show_hidden"] = "Show Hidden (%d)";
|
||||
|
||||
// Misc dialog strings
|
||||
strings_["shield_operation_id"] = "Operation ID: %s";
|
||||
strings_["peers_peer_label"] = "Peer: %s";
|
||||
strings_["key_export_click_retrieve"] = "Click to retrieve the key from your wallet";
|
||||
strings_["backup_source"] = "Source: %s";
|
||||
strings_["export_keys_progress"] = "Exporting %d/%d...";
|
||||
strings_["import_key_progress"] = "Importing %d/%d...";
|
||||
strings_["block_click_copy"] = "Click to copy";
|
||||
strings_["confirm_clear_ztx_title"] = "Confirm Clear Z-Tx History";
|
||||
strings_["confirm_clear_ztx_warning1"] = "Clearing z-transaction history may cause your shielded balance to show as 0 until a wallet rescan is performed.";
|
||||
strings_["confirm_clear_ztx_warning2"] = "If this happens, you will need to re-import your z-address private keys with rescan enabled to recover your balance.";
|
||||
|
||||
// Send tab
|
||||
strings_["sending_from"] = "SENDING FROM";
|
||||
strings_["recent_sends"] = "RECENT SENDS";
|
||||
strings_["recipient"] = "RECIPIENT";
|
||||
strings_["memo_optional"] = "MEMO (OPTIONAL)";
|
||||
strings_["no_recent_sends"] = "No recent sends";
|
||||
strings_["network_fee"] = "NETWORK FEE";
|
||||
strings_["amount_details"] = "AMOUNT DETAILS";
|
||||
strings_["not_connected_to_daemon"] = "Not connected to daemon";
|
||||
strings_["select_source_address"] = "Select a source address...";
|
||||
strings_["no_addresses_with_balance"] = "No addresses with balance";
|
||||
strings_["blockchain_syncing"] = "Blockchain syncing (%.1f%%)... Balances may be inaccurate.";
|
||||
strings_["wallet_empty"] = "Your wallet is empty";
|
||||
strings_["wallet_empty_hint"] = "Switch to Receive to get your address and start receiving funds.";
|
||||
strings_["go_to_receive"] = "Go to Receive";
|
||||
strings_["review_send"] = "Review Send";
|
||||
strings_["fee_low"] = "Low";
|
||||
strings_["fee_normal"] = "Normal";
|
||||
strings_["fee_high"] = "High";
|
||||
strings_["submitting_transaction"] = "Submitting transaction...";
|
||||
strings_["transaction_sent_msg"] = "Transaction sent!";
|
||||
strings_["copy_error"] = "Copy Error";
|
||||
strings_["dismiss"] = "Dismiss";
|
||||
strings_["clear_form_confirm"] = "Clear all form fields?";
|
||||
strings_["yes_clear"] = "Yes, Clear";
|
||||
strings_["keep"] = "Keep";
|
||||
strings_["undo_clear"] = "Undo Clear";
|
||||
|
||||
// Receive tab
|
||||
strings_["recent_received"] = "RECENT RECEIVED";
|
||||
strings_["payment_request"] = "PAYMENT REQUEST";
|
||||
strings_["copy"] = "Copy";
|
||||
strings_["new"] = "+ New";
|
||||
strings_["share"] = "Share";
|
||||
strings_["select_receiving_address"] = "Select a receiving address...";
|
||||
strings_["no_addresses_match"] = "No addresses match filter";
|
||||
strings_["no_recent_receives"] = "No recent receives";
|
||||
strings_["waiting_for_daemon"] = "Waiting for daemon connection...";
|
||||
strings_["addresses_appear_here"] = "Your receiving addresses will appear here once connected.";
|
||||
strings_["loading_addresses"] = "Loading addresses...";
|
||||
strings_["clear_request"] = "Clear Request";
|
||||
strings_["copy_uri"] = "Copy URI";
|
||||
strings_["qr_unavailable"] = "QR unavailable";
|
||||
strings_["received_label"] = "Received";
|
||||
|
||||
// Transactions tab
|
||||
strings_["no_transactions"] = "No transactions found";
|
||||
strings_["no_matching"] = "No matching transactions";
|
||||
strings_["transaction_id"] = "TRANSACTION ID";
|
||||
strings_["search_placeholder"] = "Search...";
|
||||
strings_["all_filter"] = "All";
|
||||
strings_["sent_filter"] = "Sent";
|
||||
strings_["received_filter"] = "Received";
|
||||
strings_["mined_filter"] = "Mined";
|
||||
strings_["export_csv"] = "Export CSV";
|
||||
strings_["transactions_upper"] = "TRANSACTIONS";
|
||||
strings_["received_upper"] = "RECEIVED";
|
||||
strings_["sent_upper"] = "SENT";
|
||||
strings_["mined_upper"] = "MINED";
|
||||
strings_["from_upper"] = "FROM";
|
||||
strings_["to_upper"] = "TO";
|
||||
strings_["shielded_to"] = "SHIELDED TO";
|
||||
strings_["address_upper"] = "ADDRESS";
|
||||
strings_["memo_upper"] = "MEMO";
|
||||
strings_["shielded_type"] = "Shielded";
|
||||
strings_["recv_type"] = "Recv";
|
||||
strings_["sent_type"] = "Sent";
|
||||
strings_["immature_type"] = "Immature";
|
||||
strings_["mined_type"] = "Mined";
|
||||
strings_["mature"] = "Mature";
|
||||
strings_["copy_txid"] = "Copy TxID";
|
||||
strings_["view_details"] = "View Details";
|
||||
strings_["full_details"] = "Full Details";
|
||||
strings_["showing_transactions"] = "Showing %d\xe2\x80\x93%d of %d transactions (total: %zu)";
|
||||
strings_["txs_count"] = "%d txs";
|
||||
strings_["conf_count"] = "%d conf";
|
||||
strings_["confirmations_display"] = "%d confirmations | %s";
|
||||
|
||||
// Balance Tab
|
||||
strings_["summary"] = "Summary";
|
||||
@@ -280,18 +603,455 @@ bool I18n::loadLanguage(const std::string& locale)
|
||||
strings_["warning"] = "Warning";
|
||||
strings_["amount_exceeds_balance"] = "Amount exceeds balance";
|
||||
strings_["transaction_sent"] = "Transaction sent successfully";
|
||||
|
||||
current_locale_ = "en";
|
||||
DEBUG_LOGF("Using built-in English strings (%zu strings)\n", strings_.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: reload English so we never leave stale strings
|
||||
DEBUG_LOGF("Language file not found: %s — falling back to English\n", lang_file.c_str());
|
||||
if (locale != "en") {
|
||||
loadLanguage("en");
|
||||
}
|
||||
return false;
|
||||
|
||||
// --- Common / Shared ---
|
||||
strings_["add"] = "Add";
|
||||
strings_["address_copied"] = "Address copied to clipboard";
|
||||
strings_["address_label"] = "Address:";
|
||||
strings_["amount_label"] = "Amount:";
|
||||
strings_["date_label"] = "Date:";
|
||||
strings_["delete"] = "Delete";
|
||||
strings_["fee_label"] = "Fee:";
|
||||
strings_["file_save_location"] = "File will be saved in: ~/.config/ObsidianDragon/";
|
||||
strings_["hide"] = "Hide";
|
||||
strings_["label"] = "Label:";
|
||||
strings_["loading"] = "Loading...";
|
||||
strings_["memo_label"] = "Memo:";
|
||||
strings_["notes"] = "Notes";
|
||||
strings_["notes_optional"] = "Notes (optional):";
|
||||
strings_["output_filename"] = "Output filename:";
|
||||
strings_["paste_from_clipboard"] = "Paste from Clipboard";
|
||||
strings_["show"] = "Show";
|
||||
strings_["time_days_ago"] = "%d days ago";
|
||||
strings_["time_hours_ago"] = "%d hours ago";
|
||||
strings_["time_minutes_ago"] = "%d minutes ago";
|
||||
strings_["time_seconds_ago"] = "%d seconds ago";
|
||||
strings_["unknown"] = "Unknown";
|
||||
strings_["validating"] = "Validating...";
|
||||
strings_["warning_upper"] = "WARNING!";
|
||||
|
||||
// --- About Dialog ---
|
||||
strings_["about_block_explorer"] = "Block Explorer";
|
||||
strings_["about_block_height"] = "Block Height:";
|
||||
strings_["about_build_date"] = "Build Date:";
|
||||
strings_["about_build_type"] = "Build Type:";
|
||||
strings_["about_chain"] = "Chain:";
|
||||
strings_["about_connections"] = "Connections:";
|
||||
strings_["about_credits"] = "Credits";
|
||||
strings_["about_daemon"] = "Daemon:";
|
||||
strings_["about_debug"] = "Debug";
|
||||
strings_["about_edition"] = "ImGui Edition";
|
||||
strings_["about_github"] = "GitHub";
|
||||
strings_["about_imgui"] = "ImGui:";
|
||||
strings_["about_license"] = "License";
|
||||
strings_["about_license_text"] = "This software is released under the GNU General Public License v3 (GPLv3). You are free to use, modify, and distribute this software under the terms of the license.";
|
||||
strings_["about_peers_count"] = "%zu peers";
|
||||
strings_["about_release"] = "Release";
|
||||
strings_["about_title"] = "About ObsidianDragon";
|
||||
strings_["about_version"] = "Version:";
|
||||
strings_["about_website"] = "Website";
|
||||
|
||||
// --- Address Book Dialog ---
|
||||
strings_["address_book_add"] = "Add Address";
|
||||
strings_["address_book_add_new"] = "Add New";
|
||||
strings_["address_book_added"] = "Address added to book";
|
||||
strings_["address_book_count"] = "%zu addresses saved";
|
||||
strings_["address_book_deleted"] = "Entry deleted";
|
||||
strings_["address_book_edit"] = "Edit Address";
|
||||
strings_["address_book_empty"] = "No saved addresses. Click 'Add New' to add one.";
|
||||
strings_["address_book_exists"] = "Address already exists in book";
|
||||
strings_["address_book_title"] = "Address Book";
|
||||
strings_["address_book_update_failed"] = "Failed to update - address may be duplicate";
|
||||
strings_["address_book_updated"] = "Address updated";
|
||||
|
||||
// --- Backup Dialog ---
|
||||
strings_["backup_backing_up"] = "Backing up...";
|
||||
strings_["backup_create"] = "Create Backup";
|
||||
strings_["backup_created"] = "Wallet backup created";
|
||||
strings_["backup_description"] = "Create a backup of your wallet.dat file. This file contains all your private keys and transaction history. Store the backup in a secure location.";
|
||||
strings_["backup_destination"] = "Backup destination:";
|
||||
strings_["backup_tip_external"] = "Store backups on external drives or cloud storage";
|
||||
strings_["backup_tip_multiple"] = "Create multiple backups in different locations";
|
||||
strings_["backup_tip_test"] = "Test restoring from backup periodically";
|
||||
strings_["backup_tips"] = "Tips:";
|
||||
strings_["backup_title"] = "Backup Wallet";
|
||||
strings_["backup_wallet_not_found"] = "Warning: wallet.dat not found at expected location";
|
||||
|
||||
// --- Block Info Dialog ---
|
||||
strings_["block_bits"] = "Bits:";
|
||||
strings_["block_click_next"] = "Click to view next block";
|
||||
strings_["block_click_prev"] = "Click to view previous block";
|
||||
strings_["block_get_info"] = "Get Block Info";
|
||||
strings_["block_hash"] = "Block Hash:";
|
||||
strings_["block_height"] = "Block Height:";
|
||||
strings_["block_info_title"] = "Block Information";
|
||||
strings_["block_merkle_root"] = "Merkle Root:";
|
||||
strings_["block_nav_next"] = "Next >>";
|
||||
strings_["block_nav_prev"] = "<< Previous";
|
||||
strings_["block_next"] = "Next Block:";
|
||||
strings_["block_previous"] = "Previous Block:";
|
||||
strings_["block_size"] = "Size:";
|
||||
strings_["block_timestamp"] = "Timestamp:";
|
||||
strings_["block_transactions"] = "Transactions:";
|
||||
|
||||
// --- Console Tab ---
|
||||
strings_["console_auto_scroll"] = "Auto-scroll";
|
||||
strings_["console_available_commands"] = "Available commands:";
|
||||
strings_["console_capturing_output"] = "Capturing daemon output...";
|
||||
strings_["console_clear"] = "Clear";
|
||||
strings_["console_clear_console"] = "Clear Console";
|
||||
strings_["console_cleared"] = "Console cleared";
|
||||
strings_["console_click_commands"] = "Click commands above to insert them";
|
||||
strings_["console_click_insert"] = "Click to insert";
|
||||
strings_["console_click_insert_params"] = "Click to insert with parameters";
|
||||
strings_["console_close"] = "Close";
|
||||
strings_["console_commands"] = "Commands";
|
||||
strings_["console_common_rpc"] = "Common RPC commands:";
|
||||
strings_["console_completions"] = "Completions:";
|
||||
strings_["console_connected"] = "Connected to daemon";
|
||||
strings_["console_copy_all"] = "Copy All";
|
||||
strings_["console_copy_selected"] = "Copy";
|
||||
strings_["console_daemon"] = "Daemon";
|
||||
strings_["console_daemon_error"] = "Daemon error!";
|
||||
strings_["console_daemon_started"] = "Daemon started";
|
||||
strings_["console_daemon_stopped"] = "Daemon stopped";
|
||||
strings_["console_disconnected"] = "Disconnected from daemon";
|
||||
strings_["console_errors"] = "Errors";
|
||||
strings_["console_filter_hint"] = "Filter output...";
|
||||
strings_["console_help_clear"] = " clear - Clear the console";
|
||||
strings_["console_help_getbalance"] = " getbalance - Show transparent balance";
|
||||
strings_["console_help_getblockcount"] = " getblockcount - Show current block height";
|
||||
strings_["console_help_getinfo"] = " getinfo - Show node information";
|
||||
strings_["console_help_getmininginfo"] = " getmininginfo - Show mining status";
|
||||
strings_["console_help_getpeerinfo"] = " getpeerinfo - Show connected peers";
|
||||
strings_["console_help_gettotalbalance"] = " gettotalbalance - Show total balance";
|
||||
strings_["console_help_help"] = " help - Show this help message";
|
||||
strings_["console_help_setgenerate"] = " setgenerate - Control mining";
|
||||
strings_["console_help_stop"] = " stop - Stop the daemon";
|
||||
strings_["console_line_count"] = "%zu lines";
|
||||
strings_["console_new_lines"] = "%d new lines";
|
||||
strings_["console_no_daemon"] = "No daemon";
|
||||
strings_["console_not_connected"] = "Error: Not connected to daemon";
|
||||
strings_["console_rpc_reference"] = "RPC Command Reference";
|
||||
strings_["console_search_commands"] = "Search commands...";
|
||||
strings_["console_select_all"] = "Select All";
|
||||
strings_["console_show_daemon_output"] = "Show daemon output";
|
||||
strings_["console_show_errors_only"] = "Show errors only";
|
||||
strings_["console_show_rpc_ref"] = "Show RPC command reference";
|
||||
strings_["console_showing_lines"] = "Showing %zu of %zu lines";
|
||||
strings_["console_starting_node"] = "Starting node...";
|
||||
strings_["console_status_error"] = "Error";
|
||||
strings_["console_status_running"] = "Running";
|
||||
strings_["console_status_starting"] = "Starting";
|
||||
strings_["console_status_stopped"] = "Stopped";
|
||||
strings_["console_status_stopping"] = "Stopping";
|
||||
strings_["console_status_unknown"] = "Unknown";
|
||||
strings_["console_tab_completion"] = "Tab for completion";
|
||||
strings_["console_type_help"] = "Type 'help' for available commands";
|
||||
strings_["console_welcome"] = "Welcome to ObsidianDragon Console";
|
||||
strings_["console_zoom_in"] = "Zoom in";
|
||||
strings_["console_zoom_out"] = "Zoom out";
|
||||
|
||||
// --- Export All Keys Dialog ---
|
||||
strings_["export_keys_btn"] = "Export Keys";
|
||||
strings_["export_keys_danger"] = "DANGER: This will export ALL private keys from your wallet! Anyone with access to this file can steal your funds. Store it securely and delete after use.";
|
||||
strings_["export_keys_include_t"] = "Include T-addresses (transparent)";
|
||||
strings_["export_keys_include_z"] = "Include Z-addresses (shielded)";
|
||||
strings_["export_keys_options"] = "Export options:";
|
||||
strings_["export_keys_success"] = "Keys exported successfully";
|
||||
strings_["export_keys_title"] = "Export All Private Keys";
|
||||
|
||||
// --- Export Transactions Dialog ---
|
||||
strings_["export_tx_count"] = "Export %zu transactions to CSV file.";
|
||||
strings_["export_tx_file_fail"] = "Failed to create CSV file";
|
||||
strings_["export_tx_none"] = "No transactions to export";
|
||||
strings_["export_tx_success"] = "Transactions exported successfully";
|
||||
strings_["export_tx_title"] = "Export Transactions to CSV";
|
||||
|
||||
// --- Import Key Dialog ---
|
||||
strings_["import_key_btn"] = "Import Key(s)";
|
||||
strings_["import_key_formats"] = "Supported key formats:";
|
||||
strings_["import_key_full_rescan"] = "(0 = full rescan)";
|
||||
strings_["import_key_label"] = "Private Key(s):";
|
||||
strings_["import_key_no_valid"] = "No valid keys found in input";
|
||||
strings_["import_key_rescan"] = "Rescan blockchain after import";
|
||||
strings_["import_key_start_height"] = "Start height:";
|
||||
strings_["import_key_success"] = "Keys imported successfully";
|
||||
strings_["import_key_t_format"] = "T-address WIF private keys";
|
||||
strings_["import_key_title"] = "Import Private Key";
|
||||
strings_["import_key_tooltip"] = "Enter one or more private keys, one per line.\nSupports both z-address and t-address keys.\nLines starting with # are treated as comments.";
|
||||
strings_["import_key_warning"] = "Warning: Never share your private keys! Importing keys from untrusted sources can compromise your wallet.";
|
||||
strings_["import_key_z_format"] = "Z-address spending keys (secret-extended-key-...)";
|
||||
|
||||
// --- Key Export Dialog ---
|
||||
strings_["key_export_fetching"] = "Fetching key from wallet...";
|
||||
strings_["key_export_private_key"] = "Private Key:";
|
||||
strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties.";
|
||||
strings_["key_export_reveal"] = "Reveal Key";
|
||||
strings_["key_export_viewing_key"] = "Viewing Key:";
|
||||
strings_["key_export_viewing_warning"] = "This viewing key allows others to see your incoming transactions and balance, but NOT spend your funds. Share only with trusted parties.";
|
||||
|
||||
// --- Market Tab ---
|
||||
strings_["market_12h"] = "12h";
|
||||
strings_["market_18h"] = "18h";
|
||||
strings_["market_24h"] = "24h";
|
||||
strings_["market_24h_volume"] = "24H VOLUME";
|
||||
strings_["market_6h"] = "6h";
|
||||
strings_["market_attribution"] = "Price data from NonKYC";
|
||||
strings_["market_btc_price"] = "BTC PRICE";
|
||||
strings_["market_no_history"] = "No price history available";
|
||||
strings_["market_no_price"] = "No price data";
|
||||
strings_["market_now"] = "Now";
|
||||
strings_["market_pct_shielded"] = "%.0f%% Shielded";
|
||||
strings_["market_portfolio"] = "PORTFOLIO";
|
||||
strings_["market_price_unavailable"] = "Price data unavailable";
|
||||
strings_["market_refresh_price"] = "Refresh price data";
|
||||
strings_["market_trade_on"] = "Trade on %s";
|
||||
|
||||
// --- Mining Tab ---
|
||||
strings_["mining_active"] = "Active";
|
||||
strings_["mining_address_copied"] = "Mining address copied";
|
||||
strings_["mining_all_time"] = "All Time";
|
||||
strings_["mining_already_saved"] = "Pool URL already saved";
|
||||
strings_["mining_block_copied"] = "Block hash copied";
|
||||
strings_["mining_chart_1m_ago"] = "1m ago";
|
||||
strings_["mining_chart_5m_ago"] = "5m ago";
|
||||
strings_["mining_chart_now"] = "Now";
|
||||
strings_["mining_chart_start"] = "Start";
|
||||
strings_["mining_click"] = "Click";
|
||||
strings_["mining_click_copy_address"] = "Click to copy address";
|
||||
strings_["mining_click_copy_block"] = "Click to copy block hash";
|
||||
strings_["mining_click_copy_difficulty"] = "Click to copy difficulty";
|
||||
strings_["mining_connected"] = "Connected";
|
||||
strings_["mining_connecting"] = "Connecting...";
|
||||
strings_["mining_difficulty_copied"] = "Difficulty copied";
|
||||
strings_["mining_est_block"] = "Est. Block";
|
||||
strings_["mining_est_daily"] = "Est. Daily";
|
||||
strings_["mining_filter_all"] = "All";
|
||||
strings_["mining_filter_tip_all"] = "Show all earnings";
|
||||
strings_["mining_filter_tip_pool"] = "Show pool earnings only";
|
||||
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_local_hashrate"] = "Local Hashrate";
|
||||
strings_["mining_mine"] = "Mine";
|
||||
strings_["mining_mining_addr"] = "Mining Addr";
|
||||
strings_["mining_network"] = "Network";
|
||||
strings_["mining_no_blocks_yet"] = "No blocks found yet";
|
||||
strings_["mining_no_payouts_yet"] = "No pool payouts yet";
|
||||
strings_["mining_no_saved_addresses"] = "No saved addresses";
|
||||
strings_["mining_no_saved_pools"] = "No saved pools";
|
||||
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_pool"] = "Pool";
|
||||
strings_["mining_pool_hashrate"] = "Pool Hashrate";
|
||||
strings_["mining_pool_url"] = "Pool URL";
|
||||
strings_["mining_recent_blocks"] = "RECENT BLOCKS";
|
||||
strings_["mining_recent_payouts"] = "RECENT POOL PAYOUTS";
|
||||
strings_["mining_remove"] = "Remove";
|
||||
strings_["mining_reset_defaults"] = "Reset Defaults";
|
||||
strings_["mining_save_payout_address"] = "Save payout address";
|
||||
strings_["mining_save_pool_url"] = "Save pool URL";
|
||||
strings_["mining_saved_addresses"] = "Saved Addresses:";
|
||||
strings_["mining_saved_pools"] = "Saved Pools:";
|
||||
strings_["mining_shares"] = "Shares";
|
||||
strings_["mining_show_chart"] = "Chart";
|
||||
strings_["mining_show_log"] = "Log";
|
||||
strings_["mining_solo"] = "Solo";
|
||||
strings_["mining_starting"] = "Starting...";
|
||||
strings_["mining_starting_tooltip"] = "Miner is starting...";
|
||||
strings_["mining_stop"] = "Stop";
|
||||
strings_["mining_stop_solo_for_pool"] = "Stop solo mining before starting pool mining";
|
||||
strings_["mining_stop_solo_for_pool_settings"] = "Stop solo mining to change pool settings";
|
||||
strings_["mining_stopping"] = "Stopping...";
|
||||
strings_["mining_stopping_tooltip"] = "Miner is stopping...";
|
||||
strings_["mining_syncing_tooltip"] = "Blockchain is syncing...";
|
||||
strings_["mining_to_save"] = "to save";
|
||||
strings_["mining_today"] = "Today";
|
||||
strings_["mining_uptime"] = "Uptime";
|
||||
strings_["mining_yesterday"] = "Yesterday";
|
||||
|
||||
// --- Peers Tab ---
|
||||
strings_["peers_avg_ping"] = "Avg Ping";
|
||||
strings_["peers_ban_24h"] = "Ban Peer 24h";
|
||||
strings_["peers_ban_score"] = "Ban Score: %d";
|
||||
strings_["peers_banned"] = "Banned";
|
||||
strings_["peers_banned_count"] = "Banned: %d";
|
||||
strings_["peers_best_block"] = "Best Block";
|
||||
strings_["peers_blockchain"] = "BLOCKCHAIN";
|
||||
strings_["peers_blocks"] = "Blocks";
|
||||
strings_["peers_blocks_left"] = "%d blocks left";
|
||||
strings_["peers_clear_all_bans"] = "Clear All Bans";
|
||||
strings_["peers_click_copy"] = "Click to copy";
|
||||
strings_["peers_connected"] = "Connected";
|
||||
strings_["peers_connected_count"] = "Connected: %d";
|
||||
strings_["peers_copy_ip"] = "Copy IP";
|
||||
strings_["peers_dir_in"] = "In";
|
||||
strings_["peers_dir_out"] = "Out";
|
||||
strings_["peers_hash_copied"] = "Hash copied";
|
||||
strings_["peers_hashrate"] = "Hashrate";
|
||||
strings_["peers_in_out"] = "In/Out";
|
||||
strings_["peers_longest"] = "Longest";
|
||||
strings_["peers_longest_chain"] = "Longest Chain";
|
||||
strings_["peers_memory"] = "Memory";
|
||||
strings_["peers_no_banned"] = "No banned peers";
|
||||
strings_["peers_no_connected"] = "No connected peers";
|
||||
strings_["peers_no_tls"] = "No TLS";
|
||||
strings_["peers_notarized"] = "Notarized";
|
||||
strings_["peers_p2p_port"] = "P2P Port";
|
||||
strings_["peers_protocol"] = "Protocol";
|
||||
strings_["peers_received"] = "Received";
|
||||
strings_["peers_refresh"] = "Refresh";
|
||||
strings_["peers_refresh_tooltip"] = "Refresh peer list";
|
||||
strings_["peers_refreshing"] = "Refreshing...";
|
||||
strings_["peers_sent"] = "Sent";
|
||||
strings_["peers_tt_id"] = "ID: %d";
|
||||
strings_["peers_tt_received"] = "Received: %s";
|
||||
strings_["peers_tt_sent"] = "Sent: %s";
|
||||
strings_["peers_tt_services"] = "Services: %s";
|
||||
strings_["peers_tt_start_height"] = "Start Height: %d";
|
||||
strings_["peers_tt_synced"] = "Synced H/B: %d/%d";
|
||||
strings_["peers_tt_tls_cipher"] = "TLS: %s";
|
||||
strings_["peers_unban"] = "Unban";
|
||||
strings_["peers_upper"] = "PEERS";
|
||||
strings_["peers_version"] = "Version";
|
||||
|
||||
// --- QR Popup Dialog ---
|
||||
strings_["qr_failed"] = "Failed to generate QR code";
|
||||
strings_["qr_title"] = "QR Code";
|
||||
|
||||
// --- Receive Tab ---
|
||||
strings_["click_copy_address"] = "Click to copy address";
|
||||
strings_["click_copy_uri"] = "Click to copy URI";
|
||||
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";
|
||||
strings_["new_transparent_created"] = "New transparent address created";
|
||||
strings_["payment_request_copied"] = "Payment request copied";
|
||||
strings_["payment_uri_copied"] = "Payment URI copied";
|
||||
|
||||
// --- Request Payment Dialog ---
|
||||
strings_["request_amount"] = "Amount (optional):";
|
||||
strings_["request_copy_uri"] = "Copy URI";
|
||||
strings_["request_description"] = "Generate a payment request that others can scan or copy. The QR code contains your address and optional amount/memo.";
|
||||
strings_["request_label"] = "Label (optional):";
|
||||
strings_["request_memo"] = "Memo (optional):";
|
||||
strings_["request_payment_uri"] = "Payment URI:";
|
||||
strings_["request_receive_address"] = "Receive Address:";
|
||||
strings_["request_select_address"] = "Select address...";
|
||||
strings_["request_shielded_addrs"] = "-- Shielded Addresses --";
|
||||
strings_["request_title"] = "Request Payment";
|
||||
strings_["request_transparent_addrs"] = "-- Transparent Addresses --";
|
||||
strings_["request_uri_copied"] = "Payment URI copied to clipboard";
|
||||
|
||||
// --- Send Tab ---
|
||||
strings_["send_amount"] = "Amount";
|
||||
strings_["send_amount_details"] = "AMOUNT DETAILS";
|
||||
strings_["send_amount_upper"] = "AMOUNT";
|
||||
strings_["send_clear_fields"] = "Clear all form fields?";
|
||||
strings_["send_copy_error"] = "Copy Error";
|
||||
strings_["send_dismiss"] = "Dismiss";
|
||||
strings_["send_error_copied"] = "Error copied to clipboard";
|
||||
strings_["send_error_prefix"] = "Error: %s";
|
||||
strings_["send_exceeds_available"] = "Exceeds available (%.8f)";
|
||||
strings_["send_fee"] = "Fee";
|
||||
strings_["send_fee_high"] = "High";
|
||||
strings_["send_fee_low"] = "Low";
|
||||
strings_["send_fee_normal"] = "Normal";
|
||||
strings_["send_form_restored"] = "Form restored";
|
||||
strings_["send_go_to_receive"] = "Go to Receive";
|
||||
strings_["send_keep"] = "Keep";
|
||||
strings_["send_network_fee"] = "NETWORK FEE";
|
||||
strings_["send_no_balance"] = "No balance";
|
||||
strings_["send_no_recent"] = "No recent sends";
|
||||
strings_["send_recent_sends"] = "RECENT SENDS";
|
||||
strings_["send_recipient"] = "RECIPIENT";
|
||||
strings_["send_select_source"] = "Select a source address...";
|
||||
strings_["send_sending_from"] = "SENDING FROM";
|
||||
strings_["send_submitting"] = "Submitting transaction...";
|
||||
strings_["send_switch_to_receive"] = "Switch to Receive to get your address and start receiving funds.";
|
||||
strings_["send_tooltip_enter_amount"] = "Enter an amount to send";
|
||||
strings_["send_tooltip_exceeds_balance"] = "Amount exceeds available balance";
|
||||
strings_["send_tooltip_in_progress"] = "Transaction already in progress";
|
||||
strings_["send_tooltip_invalid_address"] = "Enter a valid recipient address";
|
||||
strings_["send_tooltip_not_connected"] = "Not connected to daemon";
|
||||
strings_["send_tooltip_select_source"] = "Select a source address first";
|
||||
strings_["send_tooltip_syncing"] = "Wait for blockchain to sync";
|
||||
strings_["send_total"] = "Total";
|
||||
strings_["send_tx_failed"] = "Transaction failed";
|
||||
strings_["send_tx_sent"] = "Transaction sent!";
|
||||
strings_["send_tx_success"] = "Transaction sent successfully!";
|
||||
strings_["send_txid_copied"] = "TxID copied to clipboard";
|
||||
strings_["send_txid_label"] = "TxID: %s";
|
||||
strings_["send_valid_shielded"] = "Valid shielded address";
|
||||
strings_["send_valid_transparent"] = "Valid transparent address";
|
||||
strings_["send_wallet_empty"] = "Your wallet is empty";
|
||||
strings_["send_yes_clear"] = "Yes, Clear";
|
||||
|
||||
// --- Shield Dialog ---
|
||||
strings_["shield_check_status"] = "Check Status";
|
||||
strings_["shield_completed"] = "Operation completed successfully!";
|
||||
strings_["shield_description"] = "Shield your mining rewards by sending coinbase outputs from transparent addresses to a shielded address. This improves privacy by hiding your mining income.";
|
||||
strings_["shield_from_address"] = "From Address:";
|
||||
strings_["shield_funds"] = "Shield Funds";
|
||||
strings_["shield_in_progress"] = "Operation in progress...";
|
||||
strings_["shield_max_utxos"] = "Max UTXOs per operation";
|
||||
strings_["shield_merge_done"] = "Shield/merge completed!";
|
||||
strings_["shield_select_z"] = "Select z-address...";
|
||||
strings_["shield_started"] = "Shield operation started";
|
||||
strings_["shield_title"] = "Shield Coinbase Rewards";
|
||||
strings_["shield_to_address"] = "To Address (Shielded):";
|
||||
strings_["shield_utxo_limit"] = "UTXO Limit:";
|
||||
strings_["shield_wildcard_hint"] = "Use '*' to shield from all transparent addresses";
|
||||
strings_["merge_description"] = "Merge multiple UTXOs into a single shielded address. This can help reduce wallet size and improve privacy.";
|
||||
strings_["merge_funds"] = "Merge Funds";
|
||||
strings_["merge_started"] = "Merge operation started";
|
||||
strings_["merge_title"] = "Merge to Address";
|
||||
|
||||
// --- Transaction Details Dialog ---
|
||||
strings_["tx_confirmations"] = "%d confirmations";
|
||||
strings_["tx_details_title"] = "Transaction Details";
|
||||
strings_["tx_from_address"] = "From Address:";
|
||||
strings_["tx_id_label"] = "Transaction ID:";
|
||||
strings_["tx_immature"] = "IMMATURE";
|
||||
strings_["tx_mined"] = "MINED";
|
||||
strings_["tx_received"] = "RECEIVED";
|
||||
strings_["tx_sent"] = "SENT";
|
||||
strings_["tx_to_address"] = "To Address:";
|
||||
strings_["tx_view_explorer"] = "View on Explorer";
|
||||
|
||||
// --- Validate Address Dialog ---
|
||||
strings_["validate_btn"] = "Validate";
|
||||
strings_["validate_description"] = "Enter a DragonX address to check if it's valid and whether it belongs to this wallet.";
|
||||
strings_["validate_invalid"] = "INVALID";
|
||||
strings_["validate_is_mine"] = "This wallet owns this address";
|
||||
strings_["validate_not_mine"] = "Not owned by this wallet";
|
||||
strings_["validate_ownership"] = "Ownership:";
|
||||
strings_["validate_results"] = "Results:";
|
||||
strings_["validate_shielded_type"] = "Shielded (z-address)";
|
||||
strings_["validate_status"] = "Status:";
|
||||
strings_["validate_title"] = "Validate Address";
|
||||
strings_["validate_transparent_type"] = "Transparent (t-address)";
|
||||
strings_["validate_type"] = "Type:";
|
||||
strings_["validate_valid"] = "VALID";
|
||||
|
||||
// Misc dialog/tab strings
|
||||
strings_["ram_wallet_gb"] = "Wallet: %.1f GB";
|
||||
strings_["ram_wallet_mb"] = "Wallet: %.0f MB";
|
||||
strings_["ram_daemon_gb"] = "Daemon: %.1f GB (%s)";
|
||||
strings_["ram_daemon_mb"] = "Daemon: %.0f MB (%s)";
|
||||
strings_["ram_system_gb"] = "System: %.1f / %.0f GB";
|
||||
strings_["shield_operation_id"] = "Operation ID: %s";
|
||||
strings_["peers_peer_label"] = "Peer: %s";
|
||||
strings_["error_format"] = "Error: %s";
|
||||
strings_["key_export_click_retrieve"] = "Click to retrieve the key from your wallet";
|
||||
strings_["key_export_viewing_keys_zonly"] = "Viewing keys are only available for shielded (z) addresses";
|
||||
strings_["backup_source"] = "Source: %s";
|
||||
strings_["export_keys_progress"] = "Exporting %d/%d...";
|
||||
strings_["import_key_progress"] = "Importing %d/%d...";
|
||||
strings_["click_to_copy"] = "Click to copy";
|
||||
strings_["block_hash_copied"] = "Block hash copied";
|
||||
}
|
||||
|
||||
const char* I18n::translate(const char* key) const
|
||||
|
||||
@@ -57,6 +57,7 @@ public:
|
||||
|
||||
private:
|
||||
I18n();
|
||||
void loadBuiltinEnglish();
|
||||
|
||||
std::string current_locale_ = "en";
|
||||
std::unordered_map<std::string, std::string> strings_;
|
||||
|
||||
Reference in New Issue
Block a user