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);
|
||||
|
||||
Reference in New Issue
Block a user