diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index e4e1e01..a46551a 100644 Binary files a/res/fonts/NotoSansCJK-Subset.ttf and b/res/fonts/NotoSansCJK-Subset.ttf differ diff --git a/res/lang/de.json b/res/lang/de.json index ea03e2d..ea47d51 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -639,6 +639,11 @@ "download": "Herunterladen", "download_bootstrap": "Bootstrap herunterladen", "dragonx_green": "DragonX (Grün)", + "earnings_format": "Anzeigeformat für Erträge", + "earnings_format_coin": "Nur Münze", + "earnings_format_coin_fiat": "Münze + Fiat", + "earnings_format_fiat": "Nur Fiat", + "earnings_label": "Erträge", "edit": "Bearbeiten", "empty_wallet_keys_suffix": "Schlüssel", "empty_wallet_open_manager": "Wallet-Verwaltung öffnen", @@ -942,6 +947,7 @@ "hidden_tag": " (versteckt)", "hide": "Ausblenden", "hide_address": "Adresse ausblenden", + "hide_earnings": "Erträge ausblenden", "hide_qr": "QR ausblenden", "hide_zero_balances": "Nullsalden ausblenden", "history": "Verlauf", @@ -1920,6 +1926,7 @@ "shielded_type": "Abgeschirmt", "shielding_notice": "Hinweis: Dies wird Gelder von einer transparenten (T) Adresse auf eine private (Z) Adresse schirmen.", "show": "Anzeigen", + "show_earnings": "Erträge anzeigen", "show_hidden": "Ausgeblendete anzeigen (%d)", "show_qr": "QR anzeigen", "show_qr_code": "QR-Code anzeigen", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen", "tt_delete_blockchain": "Alle Blockchain-Daten löschen und neu synchronisieren. wallet.dat und Konfiguration bleiben erhalten.", "tt_download_bootstrap": "Blockchain-Bootstrap herunterladen, um die Synchronisierung zu beschleunigen\nVorhandene Blockdaten werden ersetzt", + "tt_earn_interval": "Ertragszeitraum: 1H = diese Stunde, 1D = heute, 1M = dieser Monat, 1Y = dieses Jahr", + "tt_earn_mode_inflows": "Ertragsmodus: erhaltene Beträge (einschließlich Mining)", + "tt_earn_mode_net": "Ertragsmodus: netto (erhalten minus gesendet)", + "tt_earn_show_all": "Ertragswert für jede Adresse anzeigen", + "tt_earnings_format": "Wie die Ertragswerte pro Adresse und insgesamt angezeigt werden", "tt_encrypt": "wallet.dat mit einer Passphrase verschlüsseln", "tt_export_all": "Alle privaten Schlüssel in eine Datei exportieren", "tt_export_csv": "Transaktionsverlauf als CSV-Tabelle exportieren", diff --git a/res/lang/es.json b/res/lang/es.json index eed85a7..1ace328 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -639,6 +639,11 @@ "download": "Descargar", "download_bootstrap": "Descargar Bootstrap", "dragonx_green": "DragonX (Verde)", + "earnings_format": "Formato de visualización de ganancias", + "earnings_format_coin": "Solo moneda", + "earnings_format_coin_fiat": "Moneda + fíat", + "earnings_format_fiat": "Solo fíat", + "earnings_label": "Ganancias", "edit": "Editar", "empty_wallet_keys_suffix": "claves", "empty_wallet_open_manager": "Abrir administrador de carteras", @@ -942,6 +947,7 @@ "hidden_tag": " (oculto)", "hide": "Ocultar", "hide_address": "Ocultar dirección", + "hide_earnings": "Ocultar ganancias", "hide_qr": "Ocultar QR", "hide_zero_balances": "Ocultar saldos 0", "history": "Historial", @@ -1920,6 +1926,7 @@ "shielded_type": "Protegido", "shielding_notice": "Nota: Esto blindará fondos de una dirección transparente (T) a una dirección privada (Z).", "show": "Mostrar", + "show_earnings": "Mostrar ganancias", "show_hidden": "Mostrar ocultos (%d)", "show_qr": "Mostrar QR", "show_qr_code": "Mostrar Código QR", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "Expandir opciones de registro de depuración", "tt_delete_blockchain": "Eliminar todos los datos de la blockchain e iniciar una nueva sincronización. Se preservan wallet.dat y la configuración.", "tt_download_bootstrap": "Descargar bootstrap de blockchain para acelerar la sincronización\nLos datos de bloques existentes serán reemplazados", + "tt_earn_interval": "Periodo de ganancias: 1H = esta hora, 1D = hoy, 1M = este mes, 1Y = este año", + "tt_earn_mode_inflows": "Modo de ganancias: importes recibidos (incluida la minería)", + "tt_earn_mode_net": "Modo de ganancias: neto (recibido menos enviado)", + "tt_earn_show_all": "Mostrar la cifra de ganancias para cada dirección", + "tt_earnings_format": "Cómo se muestran las cifras de ganancias por dirección y totales", "tt_encrypt": "Cifrar wallet.dat con una contraseña", "tt_export_all": "Exportar todas las claves privadas a un archivo", "tt_export_csv": "Exportar historial de transacciones como hoja de cálculo CSV", diff --git a/res/lang/fr.json b/res/lang/fr.json index d3a2cc4..aa30c1c 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -639,6 +639,11 @@ "download": "Télécharger", "download_bootstrap": "Télécharger Bootstrap", "dragonx_green": "DragonX (Vert)", + "earnings_format": "Format d'affichage des gains", + "earnings_format_coin": "Pièce uniquement", + "earnings_format_coin_fiat": "Pièce + fiat", + "earnings_format_fiat": "Fiat uniquement", + "earnings_label": "Gains", "edit": "Modifier", "empty_wallet_keys_suffix": "clés", "empty_wallet_open_manager": "Ouvrir le gestionnaire de portefeuilles", @@ -942,6 +947,7 @@ "hidden_tag": " (masqué)", "hide": "Masquer", "hide_address": "Masquer l'adresse", + "hide_earnings": "Masquer les gains", "hide_qr": "Masquer le QR", "hide_zero_balances": "Masquer les soldes à 0", "history": "Historique", @@ -1920,6 +1926,7 @@ "shielded_type": "Blindé", "shielding_notice": "Note : Cela blindera des fonds d'une adresse transparente (T) vers une adresse privée (Z).", "show": "Afficher", + "show_earnings": "Afficher les gains", "show_hidden": "Afficher masqués (%d)", "show_qr": "Afficher le QR", "show_qr_code": "Afficher le code QR", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "Développer les options de journalisation de débogage", "tt_delete_blockchain": "Supprimer toutes les données de la blockchain et démarrer une nouvelle synchronisation. wallet.dat et la configuration sont préservés.", "tt_download_bootstrap": "Télécharger le bootstrap blockchain pour accélérer la synchronisation\nLes données de blocs existantes seront remplacées", + "tt_earn_interval": "Période de gains : 1H = cette heure, 1D = aujourd'hui, 1M = ce mois, 1Y = cette année", + "tt_earn_mode_inflows": "Mode de gains : montants reçus (minage inclus)", + "tt_earn_mode_net": "Mode de gains : net (reçu moins envoyé)", + "tt_earn_show_all": "Afficher le montant des gains pour chaque adresse", + "tt_earnings_format": "Comment les montants de gains par adresse et totaux sont affichés", "tt_encrypt": "Chiffrer wallet.dat avec une phrase secrète", "tt_export_all": "Exporter toutes les clés privées dans un fichier", "tt_export_csv": "Exporter l'historique des transactions en feuille de calcul CSV", diff --git a/res/lang/ja.json b/res/lang/ja.json index fcb2d8c..f251b10 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -639,6 +639,11 @@ "download": "ダウンロード", "download_bootstrap": "ブートストラップをダウンロード", "dragonx_green": "DragonX(グリーン)", + "earnings_format": "収益の表示形式", + "earnings_format_coin": "コインのみ", + "earnings_format_coin_fiat": "コイン+法定通貨", + "earnings_format_fiat": "法定通貨のみ", + "earnings_label": "収益", "edit": "編集", "empty_wallet_keys_suffix": "個の鍵", "empty_wallet_open_manager": "ウォレットマネージャーを開く", @@ -942,6 +947,7 @@ "hidden_tag": " (非表示)", "hide": "非表示", "hide_address": "アドレスを非表示", + "hide_earnings": "収益を非表示", "hide_qr": "QRを非表示", "hide_zero_balances": "残高0を非表示", "history": "履歴", @@ -1920,6 +1926,7 @@ "shielded_type": "シールド", "shielding_notice": "注意:トランスペアレント (T) アドレスからプライベート (Z) アドレスへ資金をシールドします。", "show": "表示", + "show_earnings": "収益を表示", "show_hidden": "非表示を表示 (%d)", "show_qr": "QRを表示", "show_qr_code": "QRコードを表示", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "デバッグログオプションを展開", "tt_delete_blockchain": "すべてのブロックチェーンデータを削除して新規同期を開始します。wallet.dat と設定は保持されます。", "tt_download_bootstrap": "ブロックチェーンブートストラップをダウンロードして同期を高速化\n既存のブロックデータは置き換えられます", + "tt_earn_interval": "収益期間:1H=今の時間、1D=今日、1M=今月、1Y=今年", + "tt_earn_mode_inflows": "収益モード:受取額(マイニングを含む)", + "tt_earn_mode_net": "収益モード:純額(受取から送金を差し引いた額)", + "tt_earn_show_all": "すべてのアドレスの収益額を表示", + "tt_earnings_format": "アドレスごとおよび合計の収益額の表示方法", "tt_encrypt": "パスフレーズで wallet.dat を暗号化", "tt_export_all": "すべての秘密鍵をファイルにエクスポート", "tt_export_csv": "トランザクション履歴を CSV スプレッドシートとしてエクスポート", diff --git a/res/lang/ko.json b/res/lang/ko.json index 58b30f1..a32a70c 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -639,6 +639,11 @@ "download": "다운로드", "download_bootstrap": "부트스트랩 다운로드", "dragonx_green": "DragonX(그린)", + "earnings_format": "수익 표시 형식", + "earnings_format_coin": "코인만", + "earnings_format_coin_fiat": "코인 + 법정화폐", + "earnings_format_fiat": "법정화폐만", + "earnings_label": "수익", "edit": "편집", "empty_wallet_keys_suffix": "개 키", "empty_wallet_open_manager": "지갑 관리자 열기", @@ -942,6 +947,7 @@ "hidden_tag": " (숨김)", "hide": "숨기기", "hide_address": "주소 숨기기", + "hide_earnings": "수익 숨기기", "hide_qr": "QR 숨기기", "hide_zero_balances": "잔액 0 숨기기", "history": "내역", @@ -1920,6 +1926,7 @@ "shielded_type": "차폐", "shielding_notice": "참고: 투명 (T) 주소에서 프라이빗 (Z) 주소로 자금을 실딩합니다.", "show": "표시", + "show_earnings": "수익 표시", "show_hidden": "숨겨진 항목 표시 (%d)", "show_qr": "QR 표시", "show_qr_code": "QR 코드 표시", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "디버그 로깅 옵션 펼치기", "tt_delete_blockchain": "모든 블록체인 데이터를 삭제하고 새로 동기화합니다. wallet.dat 및 설정은 보존됩니다.", "tt_download_bootstrap": "블록체인 부트스트랩을 다운로드하여 동기화 가속\n기존 블록 데이터가 교체됩니다", + "tt_earn_interval": "수익 기간: 1H = 현재 시간, 1D = 오늘, 1M = 이번 달, 1Y = 올해", + "tt_earn_mode_inflows": "수익 모드: 받은 금액(채굴 포함)", + "tt_earn_mode_net": "수익 모드: 순액(받은 금액에서 보낸 금액을 뺀 값)", + "tt_earn_show_all": "모든 주소의 수익 금액 표시", + "tt_earnings_format": "주소별 및 합계 수익 금액의 표시 방식", "tt_encrypt": "비밀번호로 wallet.dat 암호화", "tt_export_all": "모든 개인키를 파일로 내보내기", "tt_export_csv": "거래 내역을 CSV 스프레드시트로 내보내기", diff --git a/res/lang/pt.json b/res/lang/pt.json index 80501e1..049c605 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -639,6 +639,11 @@ "download": "Baixar", "download_bootstrap": "Baixar Bootstrap", "dragonx_green": "DragonX (Verde)", + "earnings_format": "Formato de exibição de ganhos", + "earnings_format_coin": "Apenas moeda", + "earnings_format_coin_fiat": "Moeda + fiduciária", + "earnings_format_fiat": "Apenas fiduciária", + "earnings_label": "Ganhos", "edit": "Editar", "empty_wallet_keys_suffix": "chaves", "empty_wallet_open_manager": "Abrir gerenciador de carteiras", @@ -942,6 +947,7 @@ "hidden_tag": " (oculto)", "hide": "Ocultar", "hide_address": "Ocultar endereço", + "hide_earnings": "Ocultar ganhos", "hide_qr": "Ocultar QR", "hide_zero_balances": "Ocultar saldos zero", "history": "Histórico", @@ -1920,6 +1926,7 @@ "shielded_type": "Blindado", "shielding_notice": "Nota: Isso blindará fundos de um endereço transparente (T) para um endereço privado (Z).", "show": "Mostrar", + "show_earnings": "Mostrar ganhos", "show_hidden": "Mostrar ocultos (%d)", "show_qr": "Mostrar QR", "show_qr_code": "Mostrar Código QR", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "Expandir opções de registro de depuração", "tt_delete_blockchain": "Excluir todos os dados da blockchain e iniciar uma nova sincronização. wallet.dat e configuração são preservados.", "tt_download_bootstrap": "Baixar bootstrap da blockchain para acelerar a sincronização\nDados de blocos existentes serão substituídos", + "tt_earn_interval": "Período de ganhos: 1H = esta hora, 1D = hoje, 1M = este mês, 1Y = este ano", + "tt_earn_mode_inflows": "Modo de ganhos: valores recebidos (incluindo mineração)", + "tt_earn_mode_net": "Modo de ganhos: líquido (recebido menos enviado)", + "tt_earn_show_all": "Mostrar o valor de ganhos para cada endereço", + "tt_earnings_format": "Como os valores de ganhos por endereço e totais são exibidos", "tt_encrypt": "Encriptar wallet.dat com uma frase secreta", "tt_export_all": "Exportar todas as chaves privadas para um arquivo", "tt_export_csv": "Exportar histórico de transações como planilha CSV", diff --git a/res/lang/ru.json b/res/lang/ru.json index 96c7a91..de851e0 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -639,6 +639,11 @@ "download": "Скачать", "download_bootstrap": "Скачать бутстрап", "dragonx_green": "DragonX (Зелёная)", + "earnings_format": "Формат отображения дохода", + "earnings_format_coin": "Только монета", + "earnings_format_coin_fiat": "Монета + фиат", + "earnings_format_fiat": "Только фиат", + "earnings_label": "Доход", "edit": "Редактировать", "empty_wallet_keys_suffix": "ключей", "empty_wallet_open_manager": "Открыть менеджер кошельков", @@ -942,6 +947,7 @@ "hidden_tag": " (скрыт)", "hide": "Скрыть", "hide_address": "Скрыть адрес", + "hide_earnings": "Скрыть доход", "hide_qr": "Скрыть QR", "hide_zero_balances": "Скрыть нулевые балансы", "history": "История", @@ -1920,6 +1926,7 @@ "shielded_type": "Экранированный", "shielding_notice": "Примечание: это переведёт средства с прозрачного (T) адреса на приватный (Z) адрес.", "show": "Показать", + "show_earnings": "Показать доход", "show_hidden": "Показать скрытые (%d)", "show_qr": "Показать QR", "show_qr_code": "Показать QR-код", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "Развернуть параметры журнала отладки", "tt_delete_blockchain": "Удалить все данные блокчейна и начать синхронизацию заново. wallet.dat и конфигурация сохраняются.", "tt_download_bootstrap": "Скачать бутстрап блокчейна для ускорения синхронизации\nСуществующие данные блоков будут заменены", + "tt_earn_interval": "Период дохода: 1H — текущий час, 1D — сегодня, 1M — текущий месяц, 1Y — текущий год", + "tt_earn_mode_inflows": "Режим дохода: полученные суммы (включая майнинг)", + "tt_earn_mode_net": "Режим дохода: нетто (получено минус отправлено)", + "tt_earn_show_all": "Показывать сумму дохода для каждого адреса", + "tt_earnings_format": "Как отображаются суммы дохода по адресам и итогам", "tt_encrypt": "Зашифровать wallet.dat паролем", "tt_export_all": "Экспортировать все приватные ключи в файл", "tt_export_csv": "Экспортировать историю транзакций в виде таблицы CSV", diff --git a/res/lang/zh.json b/res/lang/zh.json index fef40fb..059178c 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -639,6 +639,11 @@ "download": "下载", "download_bootstrap": "下载引导程序", "dragonx_green": "DragonX(绿色)", + "earnings_format": "收益显示格式", + "earnings_format_coin": "仅代币", + "earnings_format_coin_fiat": "代币+法币", + "earnings_format_fiat": "仅法币", + "earnings_label": "收益", "edit": "编辑", "empty_wallet_keys_suffix": "个密钥", "empty_wallet_open_manager": "打开钱包管理器", @@ -942,6 +947,7 @@ "hidden_tag": " (已隐藏)", "hide": "隐藏", "hide_address": "隐藏地址", + "hide_earnings": "隐藏收益", "hide_qr": "隐藏二维码", "hide_zero_balances": "隐藏零余额", "history": "历史", @@ -1920,6 +1926,7 @@ "shielded_type": "屏蔽", "shielding_notice": "注意:这将把资金从透明 (T) 地址转移到隐私 (Z) 地址。", "show": "显示", + "show_earnings": "显示收益", "show_hidden": "显示已隐藏 (%d)", "show_qr": "显示二维码", "show_qr_code": "显示二维码", @@ -2068,6 +2075,11 @@ "tt_debug_expand": "展开调试日志选项", "tt_delete_blockchain": "删除所有区块链数据并重新同步。wallet.dat 和配置将被保留。", "tt_download_bootstrap": "下载区块链引导程序以加速同步\n现有区块数据将被替换", + "tt_earn_interval": "收益周期:1H=本小时,1D=今天,1M=本月,1Y=今年", + "tt_earn_mode_inflows": "收益模式:收到的金额(含挖矿)", + "tt_earn_mode_net": "收益模式:净额(收到减去发送)", + "tt_earn_show_all": "为每个地址显示收益数值", + "tt_earnings_format": "按地址和总计的收益数值的显示方式", "tt_encrypt": "使用密码加密 wallet.dat", "tt_export_all": "将所有私钥导出到文件", "tt_export_csv": "将交易历史导出为 CSV 电子表格", diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 735c90c..9707b46 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -107,6 +107,7 @@ struct SettingsPageState { float ui_opacity = 1.0f; float window_opacity = 1.0f; float favorite_opacity = 1.0f; + int earnings_format = 1; std::string balance_layout = "classic"; bool scanline_enabled = true; bool theme_effects_enabled = true; @@ -450,6 +451,7 @@ static void loadSettingsPageState(App* app) { s_settingsState.ui_opacity = settings->getUIOpacity(); s_settingsState.window_opacity = settings->getWindowOpacity(); s_settingsState.favorite_opacity = settings->getFavoriteOpacity(); + s_settingsState.earnings_format = settings->getEarningsFormat(); s_settingsState.noise_opacity = settings->getNoiseOpacity(); s_settingsState.gradient_background = settings->getGradientBackground(); @@ -535,6 +537,7 @@ static void saveSettingsPageState(config::Settings* settings) { settings->setUIOpacity(lowSpecOverrides ? realPrefs.ui_opacity : s_settingsState.ui_opacity); settings->setWindowOpacity(s_settingsState.window_opacity); // 1.0 under low-spec, as the hotkey saves it settings->setFavoriteOpacity(s_settingsState.favorite_opacity); // not low-spec gated — cosmetic + settings->setEarningsFormat(s_settingsState.earnings_format); settings->setNoiseOpacity(s_settingsState.noise_opacity); settings->setGradientBackground(s_settingsState.gradient_background); settings->setScanlineEnabled(lowSpecOverrides ? realPrefs.scanline_enabled : s_settingsState.scanline_enabled); @@ -1052,6 +1055,25 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + // --- Earnings display format (Balance tab per-address + total earnings figures) --- + { + ImGui::PushFont(body2); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(OnSurfaceMedium())); + ImGui::TextUnformatted(TR("earnings_format")); + ImGui::PopStyleColor(); + float efW = std::min(std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW), 360.0f * dp); + ImGui::SetNextItemWidth(efW); + const char* efItems[] = { TR("earnings_format_coin"), TR("earnings_format_coin_fiat"), TR("earnings_format_fiat") }; + if (ImGui::Combo("##EarningsFormat", &s_settingsState.earnings_format, efItems, IM_ARRAYSIZE(efItems))) { + s_settingsState.earnings_format = std::max(0, std::min(2, s_settingsState.earnings_format)); + if (app->settings()) { app->settings()->setEarningsFormat(s_settingsState.earnings_format); app->settings()->save(); } + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_earnings_format")); + ImGui::PopFont(); + } + + ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + // --- Effect toggles (always visible, horizontal wrapping flow) --- { ImGui::PushFont(body2); diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 8963ff5..e71d152 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include "../../data/earnings_series.h" #include #include #include @@ -56,6 +58,44 @@ void UpdateBalanceLerp(App* app) { StepDisplayLerp(s_dispUnconfirmed, state.unconfirmed_balance, dt, kBalanceLerpSpeed, instant); } +// Total period-earnings for the whole wallet, formatted per the earnings settings. Only shown when the +// global earnings toggle is on, so the default hero/layout appearance is unchanged. Own revision-keyed +// cache (independent of the per-row one in RenderSharedAddressList). +bool FormatTotalEarnings(App* app, std::string& outText, ImU32& outCol) { + using namespace material; + if (!app->settings() || !app->settings()->getEarningsShowAll()) return false; + auto& S = schema::UI(); + const auto& state = app->state(); + auto* est = app->settings(); + static dragonx::data::EarningsCache s_totalEarnCache; + auto iv = (dragonx::data::EarningsInterval)est->getEarningsInterval(); + auto md = (dragonx::data::EarningsMode)est->getEarningsMode(); + const int fmt = est->getEarningsFormat(); + const std::int64_t nowSec = (std::int64_t)std::time(nullptr); + const std::int64_t nowMs = (std::int64_t)std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + dragonx::data::EarningsKey ek; + ek.txRevision = state.tx_revision; + ek.txCount = state.transactions.size(); + ek.interval = iv; + ek.mode = md; + ek.periodStart = dragonx::data::earningsPeriodStart(nowSec, iv); + const auto& r = s_totalEarnCache.get(ek, state.transactions, nowSec, nowMs); + const double ev = r.total; + const double fiat = ev * state.market.price_usd; + const bool haveFiat = state.market.price_usd > 0.0; + const int f = haveFiat ? fmt : 0; + char buf[64]; + if (f == 0) snprintf(buf, sizeof(buf), "%+.4f", ev); + else if (f == 2) snprintf(buf, sizeof(buf), "%s$%.2f", ev < 0 ? "-" : "+", std::fabs(fiat)); + else snprintf(buf, sizeof(buf), "%+.4f ($%.2f)", ev, fiat); + outText = buf; + outCol = ev > 1e-12 ? S.resolveColor("var(--accent-shielded)", Success()) + : (ev < -1e-12 ? S.resolveColor("var(--accent-error)", ReadableError()) + : OnSurfaceDisabled()); + return true; +} + // Render compact hero line: logo + balance + USD + mining on one line void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride) { using namespace material; @@ -96,6 +136,18 @@ void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float v ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH)); ImGui::SameLine(); + // Total period-earnings (left of the USD) when the global earnings toggle is on. + { + std::string earnText; ImU32 earnCol; + if (FormatTotalEarnings(app, earnText, earnCol)) { + dl->AddText(capFont, tickerFontSize, + ImVec2(ImGui::GetCursorScreenPos().x, tickerY), earnCol, earnText.c_str()); + float ew = capFont->CalcTextSizeA(tickerFontSize, 10000, 0, earnText.c_str()).x; + ImGui::Dummy(ImVec2(ew + Layout::spacingLg(), lineH)); + ImGui::SameLine(); + } + } + // USD value const auto& state = app->state(); double usd_value = state.getBalanceUSD(); @@ -164,6 +216,29 @@ void RenderSharedAddressList(App* app, float listH, float availW, addRows(state.t_addresses, false); auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden); + // ---- Inline period-earnings: compute once (cached on tx_revision + interval/mode/period) ---- + static dragonx::data::EarningsCache s_earnCache; + const dragonx::data::EarningsResult* earnResult = nullptr; + bool earnShowAll = false; + int earnFormat = 1; // 0=coin 1=coin+fiat 2=fiat + if (app->settings()) { + auto* est = app->settings(); + earnShowAll = est->getEarningsShowAll(); + earnFormat = est->getEarningsFormat(); + auto iv = (dragonx::data::EarningsInterval)est->getEarningsInterval(); + auto md = (dragonx::data::EarningsMode)est->getEarningsMode(); + const std::int64_t nowSec = (std::int64_t)std::time(nullptr); + const std::int64_t nowMs = (std::int64_t)std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + dragonx::data::EarningsKey ek; + ek.txRevision = state.tx_revision; + ek.txCount = state.transactions.size(); + ek.interval = iv; + ek.mode = md; + ek.periodStart = dragonx::data::earningsPeriodStart(nowSec, iv); + earnResult = &s_earnCache.get(ek, state.transactions, nowSec, nowMs); + } + // ---- Toolbar: search + checkboxes + create buttons ---- float avail = ImGui::GetContentRegionAvail().x; float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f; @@ -185,39 +260,46 @@ void RenderSharedAddressList(App* app, float listH, float availW, } } - float buttonWidth = ((addrBtn.width > 0) ? addrBtn.width : 140.0f) * dp; - float spacing = ((addrBtn.gap > 0) ? addrBtn.gap : 8.0f) * dp; - float totalButtonsWidth = buttonWidth * 2 + spacing; - float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size, - S.drawElement("tabs.balance", "buttons-position").size * hs); - ImGui::SameLine(std::max(kMinButtonsPosition, - ImGui::GetWindowWidth() - totalButtonsWidth - - S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f))); + // ---- Inline period-earnings controls: interval pills (1H/1D/1M/1Y), mode toggle (received / + // net), and a global show-all toggle. The create-address buttons moved below the list (footer). ---- + if (app->settings()) { + auto* est = app->settings(); + ImFont* icoF = Type().iconSmall(); + const float segH = ImGui::GetFrameHeight(); // fixed height so pills + icon buttons align + // A compact selected/unselected toggle. `font` nullptr = default font (text pills). + auto segBtn = [&](const char* idlabel, bool selected, const char* tip, ImFont* font) -> bool { + if (font) ImGui::PushFont(font); + ImGui::PushStyleColor(ImGuiCol_Button, selected ? WithAlpha(Primary(), 70) : IM_COL32(255, 255, 255, 12)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, WithAlpha(Primary(), 110)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, WithAlpha(Primary(), 140)); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(selected ? OnSurface() : OnSurfaceMedium())); + bool clk = ImGui::Button(idlabel, ImVec2(0, segH)); + ImGui::PopStyleColor(4); + if (font) ImGui::PopFont(); + if (tip && ImGui::IsItemHovered()) material::Tooltip("%s", tip); + return clk; + }; - bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced(); - ImGui::BeginDisabled(sharedAddrSyncing); - if (s_generating_z_address) { - char genLabel[64]; - snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), material::LoadingDots()); - TactileButton(genLabel, ImVec2(buttonWidth, 0), - S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font)); - } else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), - S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { - s_generating_z_address = true; - app->createNewZAddress([](const std::string& addr) { - s_generating_z_address = false; - DEBUG_LOGF("Created new z-address: %s\n", addr.c_str()); - }); + ImGui::SameLine(0, Layout::spacingLg()); + // Interval pills + const char* ivText[4] = { "1H", "1D", "1M", "1Y" }; + int iv = est->getEarningsInterval(); + for (int k = 0; k < 4; ++k) { + char lbl[16]; snprintf(lbl, sizeof(lbl), "%s##earniv%d", ivText[k], k); + if (segBtn(lbl, iv == k, TR("tt_earn_interval"), nullptr)) { est->setEarningsInterval(k); est->save(); } + if (k < 3) ImGui::SameLine(0, 2.0f * dp); + } + // Mode toggle (received-only vs net) + ImGui::SameLine(0, Layout::spacingMd()); + int emode = est->getEarningsMode(); + if (segBtn(ICON_MD_CALL_RECEIVED "##earnmodein", emode == 0, TR("tt_earn_mode_inflows"), icoF)) { est->setEarningsMode(0); est->save(); } + ImGui::SameLine(0, 2.0f * dp); + if (segBtn(ICON_MD_SWAP_VERT "##earnmodenet", emode == 1, TR("tt_earn_mode_net"), icoF)) { est->setEarningsMode(1); est->save(); } + // Global earnings-column toggle + ImGui::SameLine(0, Layout::spacingMd()); + bool showAll = est->getEarningsShowAll(); + if (segBtn(ICON_MD_TRENDING_UP "##earnshowall", showAll, TR("tt_earn_show_all"), icoF)) { est->setEarningsShowAll(!showAll); est->save(); } } - ImGui::SameLine(); - if (TactileButton(TR("new_mining_address"), ImVec2(buttonWidth, 0), - S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { - app->createNewTAddress([](const std::string& addr) { - DEBUG_LOGF("Created new t-address: %s\n", addr.c_str()); - }); - } - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_new_mining_address")); - ImGui::EndDisabled(); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); @@ -279,6 +361,7 @@ 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()); + ImU32 earnNegCol = S.resolveColor("var(--accent-error)", material::ReadableError()); // negative net earnings float rowPadLeft = Layout::cardInnerPadding(); float rowPadRight = Layout::cardInnerPadding(); float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, @@ -594,9 +677,33 @@ void RenderSharedAddressList(App* app, float listH, float availW, } } + // ---- Line-2 right content widths: USD + optional period-earnings to its left ---- + std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd); + float usdW = usdText.empty() ? 0.0f + : capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str()).x; + const bool haveFiat = state.market.price_usd > 0.0; + bool earnShown = earnResult && app->settings() && + (earnShowAll || app->settings()->isAddressEarningsShown(addr.address)); + char earnBuf[48]; earnBuf[0] = '\0'; + float earnW = 0.0f; ImU32 earnCol = OnSurfaceDisabled(); + if (earnShown) { + double ev = dragonx::data::earningsForAddress(*earnResult, addr.address); + double fiat = ev * state.market.price_usd; + int fmt = (!haveFiat) ? 0 : earnFormat; // no price -> coin only + if (fmt == 0) snprintf(earnBuf, sizeof(earnBuf), "%+.4f", ev); + else if (fmt == 2) snprintf(earnBuf, sizeof(earnBuf), "%s$%.2f", ev < 0 ? "-" : "+", std::fabs(fiat)); + else snprintf(earnBuf, sizeof(earnBuf), "%+.4f ($%.2f)", ev, fiat); + earnW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, earnBuf).x; + earnCol = ev > 1e-12 ? greenCol : (ev < -1e-12 ? earnNegCol : OnSurfaceDisabled()); + if (row.hidden) earnCol = OnSurfaceDisabled(); + } + // Left edge of the line-2 right content, so the address truncates before it. + const float line2RightStart = contentRight - usdW + - (earnShown && earnBuf[0] ? (earnW + Layout::spacingMd()) : 0.0f); + // ---- Address (second line) ---- { - float addrAvailW = contentRight - labelX; + float addrAvailW = line2RightStart - Layout::spacingMd() - labelX; ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str()); std::string display_addr; if (fullAddrSz.x <= addrAvailW) { @@ -624,14 +731,17 @@ void RenderSharedAddressList(App* app, float listH, float availW, DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf); } - // ---- USD value (second line, right-aligned) ---- + // ---- Line 2 right: period-earnings (left of) + USD value, right-aligned ---- { - std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd); - if (!usdText.empty()) { - ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str()); + const float line2Y = cy + body2->LegacySize + Layout::spacingXs(); + if (earnShown && earnBuf[0]) { dl->AddText(capFont, capFont->LegacySize, - ImVec2(contentRight - usdSz.x, - cy + body2->LegacySize + Layout::spacingXs()), + ImVec2(contentRight - usdW - Layout::spacingMd() - earnW, line2Y), + earnCol, earnBuf); + } + if (!usdText.empty()) { + dl->AddText(capFont, capFont->LegacySize, + ImVec2(contentRight - usdW, line2Y), OnSurfaceDisabled(), usdText.c_str()); } } @@ -728,6 +838,15 @@ void RenderSharedAddressList(App* app, float listH, float availW, if (ImGui::MenuItem(TR("favorite_address"))) app->favoriteAddress(addr.address); } + // Per-address earnings column toggle (when the global show-all toggle is off). + if (app->settings()) { + const bool shown = app->settings()->isAddressEarningsShown(addr.address); + if (ImGui::MenuItem(shown ? TR("hide_earnings") : TR("show_earnings"))) { + if (shown) app->settings()->hideAddressEarnings(addr.address); + else app->settings()->showAddressEarnings(addr.address); + app->settings()->save(); + } + } // Portfolio membership — toggle this address in each Market-tab portfolio entry. ImGui::Separator(); if (ImGui::BeginMenu(TR("portfolio_add_to"))) { @@ -818,7 +937,8 @@ void RenderSharedAddressList(App* app, float listH, float availW, listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY); } - // Address count + // Address count + create-address buttons (relocated below the list to free the toolbar row for + // the earnings controls). Count on the left, New Z / New mining right-aligned on the same row. { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); char countBuf[128]; @@ -826,6 +946,38 @@ void RenderSharedAddressList(App* app, float listH, float availW, snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"), (int)rows.size(), totalAddrs); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf); + + float buttonWidth = ((addrBtn.width > 0) ? addrBtn.width : 140.0f) * dp; + float spacing = ((addrBtn.gap > 0) ? addrBtn.gap : 8.0f) * dp; + float totalButtonsWidth = buttonWidth * 2 + spacing; + float rightMargin = S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f); + ImGui::SameLine(std::max(ImGui::GetCursorPosX(), + ImGui::GetWindowWidth() - totalButtonsWidth - rightMargin)); + + bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced(); + ImGui::BeginDisabled(sharedAddrSyncing); + if (s_generating_z_address) { + char genLabel[64]; + snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), material::LoadingDots()); + TactileButton(genLabel, ImVec2(buttonWidth, 0), + S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font)); + } else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), + S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { + s_generating_z_address = true; + app->createNewZAddress([](const std::string& addr) { + s_generating_z_address = false; + DEBUG_LOGF("Created new z-address: %s\n", addr.c_str()); + }); + } + ImGui::SameLine(); + if (TactileButton(TR("new_mining_address"), ImVec2(buttonWidth, 0), + S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { + app->createNewTAddress([](const std::string& addr) { + DEBUG_LOGF("Created new t-address: %s\n", addr.c_str()); + }); + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_new_mining_address")); + ImGui::EndDisabled(); } } diff --git a/src/ui/windows/balance_components.h b/src/ui/windows/balance_components.h index fb028ac..c25fa70 100644 --- a/src/ui/windows/balance_components.h +++ b/src/ui/windows/balance_components.h @@ -8,6 +8,8 @@ #pragma once +#include + #include "imgui.h" namespace dragonx { @@ -31,5 +33,10 @@ void RenderSharedAddressList(App* app, float listH, float availW, float glassRou void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs); void RenderSyncBar(App* app, ImDrawList* dl, float vs); +// Total period-earnings for the whole wallet, formatted per the earnings settings. Returns false +// (and leaves out* untouched) when the global earnings toggle is off, so layouts show it only when +// the user has opted into the earnings view. Shared by RenderCompactHero and the layout totals. +bool FormatTotalEarnings(App* app, std::string& outText, ImU32& outCol); + } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index 86bd58b..23a7f2e 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -364,7 +364,7 @@ static void RenderBalanceClassic(App* app) OnSurfaceMedium(), DRAGONX_TICKER); cy += sub1->LegacySize + valGap; - // USD value + // USD value (+ total period-earnings to its right when the earnings toggle is on) { double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) @@ -373,6 +373,12 @@ static void RenderBalanceClassic(App* app) snprintf(buf, sizeof(buf), "$-.-- USD"); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf); + std::string earnText; ImU32 earnCol; + if (FormatTotalEarnings(app, earnText, earnCol)) { + float usdW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf).x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(cx + usdW + tickGap * 3.0f, cy), earnCol, earnText.c_str()); + } } cy += capFont->LegacySize + 2 * dp; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 1434aa0..f132766 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1065,6 +1065,18 @@ void I18n::loadBuiltinEnglish() strings_["filter"] = "Filter..."; strings_["no_addresses_yet"] = "No addresses yet"; strings_["showing_x_of_y"] = "Showing %d of %d addresses"; + strings_["tt_earn_interval"] = "Earnings period: 1H = this hour, 1D = today, 1M = this month, 1Y = this year"; + strings_["tt_earn_mode_inflows"] = "Earnings mode: amounts received (including mined)"; + strings_["tt_earn_mode_net"] = "Earnings mode: net (received minus sent)"; + strings_["tt_earn_show_all"] = "Show the earnings figure for every address"; + strings_["show_earnings"] = "Show earnings"; + strings_["hide_earnings"] = "Hide earnings"; + strings_["earnings_label"] = "Earnings"; + strings_["earnings_format"] = "Earnings Display Format"; + strings_["tt_earnings_format"] = "How the per-address and total earnings figures are shown"; + strings_["earnings_format_coin"] = "Coin only"; + strings_["earnings_format_coin_fiat"] = "Coin + fiat"; + strings_["earnings_format_fiat"] = "Fiat only"; strings_["set_label"] = "Set Label"; strings_["copied"] = "Copied!"; strings_["hidden_tag"] = " (hidden)";