feat(market): portfolio row-style redesign + per-wallet scope
Replace the drag/resize dashboard grid on the Market tab's portfolio with
three full-width row styles — compact (single-line), detailed (two-line),
and value-hero — cycled with Left/Right arrows like the Overview layouts.
Rows live in an internal scroll region bounded to the portfolio's height
budget, so the section stays put as groups are added.
- pfDrawRow renders each group in the selected style, reusing the icon /
value / DRGX / 24h / sparkline pieces; per-entry field toggles honored.
- portfolio_style setting (0/1/2) persisted; arrow-key switcher + toast.
- Portfolio groups gain a per-wallet scope: new groups default to the
active wallet's identity hash; legacy groups (empty scope) stay visible
in every wallet. Scope filter drives both the height budget and the rows.
- Remove the now-dead grid machinery (mktDrawCard, pfComputeGridLayout,
PfCell, PfGridDrag, MktCardCtx + MktCtx grid fields) — net code drop.
- Full UI sweep: seed demo portfolio groups + wavy price history and add
market-rows-{compact,detailed,featured} surfaces so the styles are
captured per skin (snapshot/restore the real portfolio + market data).
- i18n: portfolio_style_{label,compact,detailed,featured} + 8 langs; CJK
subset rebuilt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "Minute",
|
"portfolio_spark_min": "Minute",
|
||||||
"portfolio_spark_month": "Monat",
|
"portfolio_spark_month": "Monat",
|
||||||
"portfolio_spark_week": "Woche",
|
"portfolio_spark_week": "Woche",
|
||||||
|
"portfolio_style_compact": "Kompakte Zeilen",
|
||||||
|
"portfolio_style_detailed": "Detaillierte Zeilen",
|
||||||
|
"portfolio_style_featured": "Hervorgehobene Zeilen",
|
||||||
|
"portfolio_style_label": "Portfolio-Stil",
|
||||||
"portfolio_untitled": "Ohne Titel",
|
"portfolio_untitled": "Ohne Titel",
|
||||||
"price_chart": "Preisdiagramm",
|
"price_chart": "Preisdiagramm",
|
||||||
"privacy_great": "Großartige Privatsphäre!",
|
"privacy_great": "Großartige Privatsphäre!",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "Name (z. B. Ersparnisse)",
|
"wallets_new_hint": "Name (z. B. Ersparnisse)",
|
||||||
"wallets_new_label": "Eine neue Wallet erstellen:",
|
"wallets_new_label": "Eine neue Wallet erstellen:",
|
||||||
"wallets_open": "Öffnen",
|
"wallets_open": "Öffnen",
|
||||||
|
"wallets_reveal": "Ordner anzeigen",
|
||||||
"wallets_title": "Wallets",
|
"wallets_title": "Wallets",
|
||||||
"warning": "Warnung",
|
"warning": "Warnung",
|
||||||
"warning_upper": "WARNUNG!",
|
"warning_upper": "WARNUNG!",
|
||||||
|
|||||||
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "Minuto",
|
"portfolio_spark_min": "Minuto",
|
||||||
"portfolio_spark_month": "Mes",
|
"portfolio_spark_month": "Mes",
|
||||||
"portfolio_spark_week": "Semana",
|
"portfolio_spark_week": "Semana",
|
||||||
|
"portfolio_style_compact": "Filas compactas",
|
||||||
|
"portfolio_style_detailed": "Filas detalladas",
|
||||||
|
"portfolio_style_featured": "Filas destacadas",
|
||||||
|
"portfolio_style_label": "Estilo de cartera",
|
||||||
"portfolio_untitled": "Sin título",
|
"portfolio_untitled": "Sin título",
|
||||||
"price_chart": "Gráfico de Precios",
|
"price_chart": "Gráfico de Precios",
|
||||||
"privacy_great": "¡Excelente privacidad!",
|
"privacy_great": "¡Excelente privacidad!",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "Nombre (p. ej. ahorros)",
|
"wallets_new_hint": "Nombre (p. ej. ahorros)",
|
||||||
"wallets_new_label": "Crear una nueva cartera:",
|
"wallets_new_label": "Crear una nueva cartera:",
|
||||||
"wallets_open": "Abrir",
|
"wallets_open": "Abrir",
|
||||||
|
"wallets_reveal": "Mostrar carpeta",
|
||||||
"wallets_title": "Carteras",
|
"wallets_title": "Carteras",
|
||||||
"warning": "Advertencia",
|
"warning": "Advertencia",
|
||||||
"warning_upper": "¡ADVERTENCIA!",
|
"warning_upper": "¡ADVERTENCIA!",
|
||||||
|
|||||||
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "Minute",
|
"portfolio_spark_min": "Minute",
|
||||||
"portfolio_spark_month": "Mois",
|
"portfolio_spark_month": "Mois",
|
||||||
"portfolio_spark_week": "Semaine",
|
"portfolio_spark_week": "Semaine",
|
||||||
|
"portfolio_style_compact": "Lignes compactes",
|
||||||
|
"portfolio_style_detailed": "Lignes détaillées",
|
||||||
|
"portfolio_style_featured": "Lignes en vedette",
|
||||||
|
"portfolio_style_label": "Style du portefeuille",
|
||||||
"portfolio_untitled": "Sans titre",
|
"portfolio_untitled": "Sans titre",
|
||||||
"price_chart": "Graphique des prix",
|
"price_chart": "Graphique des prix",
|
||||||
"privacy_great": "Excellente confidentialité !",
|
"privacy_great": "Excellente confidentialité !",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "Nom (par ex. épargne)",
|
"wallets_new_hint": "Nom (par ex. épargne)",
|
||||||
"wallets_new_label": "Créer un nouveau portefeuille :",
|
"wallets_new_label": "Créer un nouveau portefeuille :",
|
||||||
"wallets_open": "Ouvrir",
|
"wallets_open": "Ouvrir",
|
||||||
|
"wallets_reveal": "Afficher le dossier",
|
||||||
"wallets_title": "Portefeuilles",
|
"wallets_title": "Portefeuilles",
|
||||||
"warning": "Attention",
|
"warning": "Attention",
|
||||||
"warning_upper": "ATTENTION !",
|
"warning_upper": "ATTENTION !",
|
||||||
|
|||||||
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "分",
|
"portfolio_spark_min": "分",
|
||||||
"portfolio_spark_month": "月",
|
"portfolio_spark_month": "月",
|
||||||
"portfolio_spark_week": "週",
|
"portfolio_spark_week": "週",
|
||||||
|
"portfolio_style_compact": "コンパクト行",
|
||||||
|
"portfolio_style_detailed": "詳細行",
|
||||||
|
"portfolio_style_featured": "注目行",
|
||||||
|
"portfolio_style_label": "ポートフォリオスタイル",
|
||||||
"portfolio_untitled": "無題",
|
"portfolio_untitled": "無題",
|
||||||
"price_chart": "価格チャート",
|
"price_chart": "価格チャート",
|
||||||
"privacy_great": "優れたプライバシーです!",
|
"privacy_great": "優れたプライバシーです!",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "名前(例:savings)",
|
"wallets_new_hint": "名前(例:savings)",
|
||||||
"wallets_new_label": "新しいウォレットを作成:",
|
"wallets_new_label": "新しいウォレットを作成:",
|
||||||
"wallets_open": "開く",
|
"wallets_open": "開く",
|
||||||
|
"wallets_reveal": "フォルダを開く",
|
||||||
"wallets_title": "ウォレット",
|
"wallets_title": "ウォレット",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"warning_upper": "警告!",
|
"warning_upper": "警告!",
|
||||||
|
|||||||
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "분",
|
"portfolio_spark_min": "분",
|
||||||
"portfolio_spark_month": "월",
|
"portfolio_spark_month": "월",
|
||||||
"portfolio_spark_week": "주",
|
"portfolio_spark_week": "주",
|
||||||
|
"portfolio_style_compact": "간결한 행",
|
||||||
|
"portfolio_style_detailed": "상세 행",
|
||||||
|
"portfolio_style_featured": "강조 행",
|
||||||
|
"portfolio_style_label": "포트폴리오 스타일",
|
||||||
"portfolio_untitled": "제목 없음",
|
"portfolio_untitled": "제목 없음",
|
||||||
"price_chart": "가격 차트",
|
"price_chart": "가격 차트",
|
||||||
"privacy_great": "프라이버시가 우수합니다!",
|
"privacy_great": "프라이버시가 우수합니다!",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "이름(예: savings)",
|
"wallets_new_hint": "이름(예: savings)",
|
||||||
"wallets_new_label": "새 지갑 만들기:",
|
"wallets_new_label": "새 지갑 만들기:",
|
||||||
"wallets_open": "열기",
|
"wallets_open": "열기",
|
||||||
|
"wallets_reveal": "폴더 열기",
|
||||||
"wallets_title": "지갑",
|
"wallets_title": "지갑",
|
||||||
"warning": "경고",
|
"warning": "경고",
|
||||||
"warning_upper": "경고!",
|
"warning_upper": "경고!",
|
||||||
|
|||||||
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "Minuto",
|
"portfolio_spark_min": "Minuto",
|
||||||
"portfolio_spark_month": "Mês",
|
"portfolio_spark_month": "Mês",
|
||||||
"portfolio_spark_week": "Semana",
|
"portfolio_spark_week": "Semana",
|
||||||
|
"portfolio_style_compact": "Linhas compactas",
|
||||||
|
"portfolio_style_detailed": "Linhas detalhadas",
|
||||||
|
"portfolio_style_featured": "Linhas em destaque",
|
||||||
|
"portfolio_style_label": "Estilo do portfólio",
|
||||||
"portfolio_untitled": "Sem título",
|
"portfolio_untitled": "Sem título",
|
||||||
"price_chart": "Gráfico de Preços",
|
"price_chart": "Gráfico de Preços",
|
||||||
"privacy_great": "Ótima privacidade!",
|
"privacy_great": "Ótima privacidade!",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "Nome (ex.: poupança)",
|
"wallets_new_hint": "Nome (ex.: poupança)",
|
||||||
"wallets_new_label": "Criar uma nova carteira:",
|
"wallets_new_label": "Criar uma nova carteira:",
|
||||||
"wallets_open": "Abrir",
|
"wallets_open": "Abrir",
|
||||||
|
"wallets_reveal": "Mostrar pasta",
|
||||||
"wallets_title": "Carteiras",
|
"wallets_title": "Carteiras",
|
||||||
"warning": "Aviso",
|
"warning": "Aviso",
|
||||||
"warning_upper": "AVISO!",
|
"warning_upper": "AVISO!",
|
||||||
|
|||||||
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "Минута",
|
"portfolio_spark_min": "Минута",
|
||||||
"portfolio_spark_month": "Месяц",
|
"portfolio_spark_month": "Месяц",
|
||||||
"portfolio_spark_week": "Неделя",
|
"portfolio_spark_week": "Неделя",
|
||||||
|
"portfolio_style_compact": "Компактные строки",
|
||||||
|
"portfolio_style_detailed": "Подробные строки",
|
||||||
|
"portfolio_style_featured": "Избранные строки",
|
||||||
|
"portfolio_style_label": "Стиль портфеля",
|
||||||
"portfolio_untitled": "Без названия",
|
"portfolio_untitled": "Без названия",
|
||||||
"price_chart": "График цен",
|
"price_chart": "График цен",
|
||||||
"privacy_great": "Отличная конфиденциальность!",
|
"privacy_great": "Отличная конфиденциальность!",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "Имя (например, savings)",
|
"wallets_new_hint": "Имя (например, savings)",
|
||||||
"wallets_new_label": "Создать новый кошелёк:",
|
"wallets_new_label": "Создать новый кошелёк:",
|
||||||
"wallets_open": "Открыть",
|
"wallets_open": "Открыть",
|
||||||
|
"wallets_reveal": "Открыть папку",
|
||||||
"wallets_title": "Кошельки",
|
"wallets_title": "Кошельки",
|
||||||
"warning": "Предупреждение",
|
"warning": "Предупреждение",
|
||||||
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
|
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
|
||||||
|
|||||||
@@ -839,6 +839,10 @@
|
|||||||
"portfolio_spark_min": "分钟",
|
"portfolio_spark_min": "分钟",
|
||||||
"portfolio_spark_month": "月",
|
"portfolio_spark_month": "月",
|
||||||
"portfolio_spark_week": "周",
|
"portfolio_spark_week": "周",
|
||||||
|
"portfolio_style_compact": "紧凑行",
|
||||||
|
"portfolio_style_detailed": "详细行",
|
||||||
|
"portfolio_style_featured": "特色行",
|
||||||
|
"portfolio_style_label": "投资组合样式",
|
||||||
"portfolio_untitled": "未命名",
|
"portfolio_untitled": "未命名",
|
||||||
"price_chart": "价格图表",
|
"price_chart": "价格图表",
|
||||||
"privacy_great": "隐私性极佳!",
|
"privacy_great": "隐私性极佳!",
|
||||||
@@ -1341,6 +1345,7 @@
|
|||||||
"wallets_new_hint": "名称(例如 savings)",
|
"wallets_new_hint": "名称(例如 savings)",
|
||||||
"wallets_new_label": "创建新钱包:",
|
"wallets_new_label": "创建新钱包:",
|
||||||
"wallets_open": "打开",
|
"wallets_open": "打开",
|
||||||
|
"wallets_reveal": "打开文件夹",
|
||||||
"wallets_title": "钱包",
|
"wallets_title": "钱包",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"warning_upper": "警告!",
|
"warning_upper": "警告!",
|
||||||
|
|||||||
@@ -833,6 +833,7 @@ private:
|
|||||||
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
|
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
|
||||||
std::vector<TransactionInfo> transactions;
|
std::vector<TransactionInfo> transactions;
|
||||||
double market_price_usd=0;
|
double market_price_usd=0;
|
||||||
|
double market_change_24h=0;
|
||||||
} sweep_state_snapshot_;
|
} sweep_state_snapshot_;
|
||||||
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||||||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
|
||||||
|
#include "config/settings.h"
|
||||||
#include "ui/schema/skin_manager.h"
|
#include "ui/schema/skin_manager.h"
|
||||||
#include "ui/notifications.h"
|
#include "ui/notifications.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
|
|
||||||
#include <sodium.h>
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
@@ -31,6 +33,13 @@ namespace dragonx {
|
|||||||
namespace {
|
namespace {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are
|
||||||
|
// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct.
|
||||||
|
std::vector<config::Settings::PortfolioEntry> g_pfSnapshot;
|
||||||
|
int g_pfStyleSnapshot = 0;
|
||||||
|
bool g_pfSnapshotValid = false;
|
||||||
|
std::vector<double> g_marketHistorySnapshot; // real price history, restored at sweep end
|
||||||
|
|
||||||
// Filesystem-safe one-word tab name.
|
// Filesystem-safe one-word tab name.
|
||||||
const char* sweepPageName(ui::NavPage page)
|
const char* sweepPageName(ui::NavPage page)
|
||||||
{
|
{
|
||||||
@@ -93,6 +102,7 @@ void App::installDemoWalletData()
|
|||||||
s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
|
s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
|
||||||
s.transactions = state_.transactions;
|
s.transactions = state_.transactions;
|
||||||
s.market_price_usd = state_.market.price_usd;
|
s.market_price_usd = state_.market.price_usd;
|
||||||
|
s.market_change_24h = state_.market.change_24h;
|
||||||
s.valid = true;
|
s.valid = true;
|
||||||
|
|
||||||
applyHealthyDemoState();
|
applyHealthyDemoState();
|
||||||
@@ -102,6 +112,18 @@ void App::installDemoWalletData()
|
|||||||
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
|
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
|
||||||
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
|
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
|
||||||
state_.market.price_usd = 0.01336200;
|
state_.market.price_usd = 0.01336200;
|
||||||
|
state_.market.change_24h = 5.24000000;
|
||||||
|
// Wavy, gently-rising price history so the row sparklines render (restored at sweep end).
|
||||||
|
g_marketHistorySnapshot = state_.market.price_history;
|
||||||
|
{
|
||||||
|
std::vector<double> hist;
|
||||||
|
for (int i = 0; i < 48; i++) {
|
||||||
|
double t = static_cast<double>(i);
|
||||||
|
hist.push_back(0.01250 + 0.0000180 * t
|
||||||
|
+ 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7));
|
||||||
|
}
|
||||||
|
state_.market.price_history = hist;
|
||||||
|
}
|
||||||
|
|
||||||
auto zaddr = [](const char* a, double bal, const char* label) {
|
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
||||||
@@ -135,6 +157,32 @@ void App::installDemoWalletData()
|
|||||||
1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
|
1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot
|
||||||
|
// the user's real portfolio first; setPortfolio* only mutate memory (save() is never called
|
||||||
|
// here), and clearDemoWalletData() restores them at sweep end.
|
||||||
|
if (settings_) {
|
||||||
|
g_pfSnapshot = settings_->getPortfolioEntries();
|
||||||
|
g_pfStyleSnapshot = settings_->getPortfolioStyle();
|
||||||
|
g_pfSnapshotValid = true;
|
||||||
|
auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr,
|
||||||
|
bool drgx, bool value, bool ch, bool spark) {
|
||||||
|
config::Settings::PortfolioEntry e;
|
||||||
|
e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30;
|
||||||
|
e.addresses = { addr }; e.priceBasis = 0;
|
||||||
|
e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark;
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
settings_->setPortfolioEntries({
|
||||||
|
grp("Savings", "savings", 0xFFFF9D4Fu,
|
||||||
|
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true),
|
||||||
|
grp("Mining rewards", "pickaxe", 0xFF3DB0FFu,
|
||||||
|
"t1DemoTransparentAddressForUiSweep00000", true, true, true, true),
|
||||||
|
grp("Cold storage", "diamond", 0xFFFF7BB0u,
|
||||||
|
"zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false),
|
||||||
|
});
|
||||||
|
settings_->setPortfolioStyle(0);
|
||||||
|
}
|
||||||
|
|
||||||
seedChatDemoData();
|
seedChatDemoData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +216,18 @@ void App::clearDemoWalletData()
|
|||||||
state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
|
state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
|
||||||
state_.transactions = s.transactions;
|
state_.transactions = s.transactions;
|
||||||
state_.market.price_usd = s.market_price_usd;
|
state_.market.price_usd = s.market_price_usd;
|
||||||
|
state_.market.change_24h = s.market_change_24h;
|
||||||
|
state_.market.price_history = g_marketHistorySnapshot;
|
||||||
|
g_marketHistorySnapshot.clear();
|
||||||
s = SweepStateSnapshot{}; // invalidate
|
s = SweepStateSnapshot{}; // invalidate
|
||||||
|
|
||||||
|
// Restore the user's real portfolio (demo groups were in-memory only).
|
||||||
|
if (g_pfSnapshotValid && settings_) {
|
||||||
|
settings_->setPortfolioEntries(g_pfSnapshot);
|
||||||
|
settings_->setPortfolioStyle(g_pfStyleSnapshot);
|
||||||
|
}
|
||||||
|
g_pfSnapshot.clear();
|
||||||
|
g_pfSnapshotValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
|
// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -326,6 +385,17 @@ void App::buildSweepCatalog()
|
|||||||
add("popup-send-confirm", ui::NavPage::Send,
|
add("popup-send-confirm", ui::NavPage::Send,
|
||||||
[](App& a) { ui::SweepShowSendConfirm(&a, true); },
|
[](App& a) { ui::SweepShowSendConfirm(&a, true); },
|
||||||
[](App& a) { ui::SweepShowSendConfirm(&a, false); });
|
[](App& a) { ui::SweepShowSendConfirm(&a, false); });
|
||||||
|
|
||||||
|
// Market portfolio row styles — capture all three so the redesign is reviewable per skin.
|
||||||
|
add("market-rows-compact", ui::NavPage::Market,
|
||||||
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); },
|
||||||
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||||
|
add("market-rows-detailed", ui::NavPage::Market,
|
||||||
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); },
|
||||||
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||||
|
add("market-rows-featured", ui::NavPage::Market,
|
||||||
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); },
|
||||||
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── State machine ───────────────────────────────────────────────────────────────────────────
|
// ── State machine ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ bool Settings::load(const std::string& path)
|
|||||||
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||||
|
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||||
hidden_addresses_.clear();
|
hidden_addresses_.clear();
|
||||||
@@ -344,6 +346,7 @@ bool Settings::load(const std::string& path)
|
|||||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||||
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||||
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
||||||
|
entry.scope = e.value("scope", "");
|
||||||
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||||
entry.gridCol = e["grid_col"].get<int>();
|
entry.gridCol = e["grid_col"].get<int>();
|
||||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||||
@@ -418,6 +421,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["ui_opacity"] = ui_opacity_;
|
j["ui_opacity"] = ui_opacity_;
|
||||||
j["window_opacity"] = window_opacity_;
|
j["window_opacity"] = window_opacity_;
|
||||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||||
|
j["portfolio_style"] = portfolio_style_;
|
||||||
j["scanline_enabled"] = scanline_enabled_;
|
j["scanline_enabled"] = scanline_enabled_;
|
||||||
j["hidden_addresses"] = json::array();
|
j["hidden_addresses"] = json::array();
|
||||||
for (const auto& addr : hidden_addresses_)
|
for (const auto& addr : hidden_addresses_)
|
||||||
@@ -520,6 +524,7 @@ bool Settings::save(const std::string& path)
|
|||||||
entry["show_24h"] = e.show24h;
|
entry["show_24h"] = e.show24h;
|
||||||
entry["show_sparkline"] = e.showSparkline;
|
entry["show_sparkline"] = e.showSparkline;
|
||||||
entry["sparkline_interval"] = e.sparklineInterval;
|
entry["sparkline_interval"] = e.sparklineInterval;
|
||||||
|
entry["scope"] = e.scope;
|
||||||
entry["grid_col"] = e.gridCol;
|
entry["grid_col"] = e.gridCol;
|
||||||
entry["grid_row"] = e.gridRow;
|
entry["grid_row"] = e.gridRow;
|
||||||
entry["grid_w"] = e.gridW;
|
entry["grid_w"] = e.gridW;
|
||||||
|
|||||||
@@ -92,12 +92,16 @@ public:
|
|||||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||||
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||||
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
|
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
|
||||||
// Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells
|
// Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity
|
||||||
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
|
// hash (shown only when that wallet is active). New entries are tagged with the current
|
||||||
|
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
|
||||||
|
std::string scope;
|
||||||
|
// Legacy dashboard-grid placement (deprecated by the row layout; kept for back-compat so an
|
||||||
|
// older build's saved positions aren't dropped, but no longer used by the renderer).
|
||||||
int gridCol = -1;
|
int gridCol = -1;
|
||||||
int gridRow = -1;
|
int gridRow = -1;
|
||||||
int gridW = 8; // default group width (cells); minimum is 8
|
int gridW = 8;
|
||||||
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
|
int gridH = 3;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
@@ -176,6 +180,11 @@ public:
|
|||||||
std::string getBalanceLayout() const { return balance_layout_; }
|
std::string getBalanceLayout() const { return balance_layout_; }
|
||||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||||
|
|
||||||
|
// Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with
|
||||||
|
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||||
|
int getPortfolioStyle() const { return portfolio_style_; }
|
||||||
|
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
|
|
||||||
// Console scanline effect
|
// Console scanline effect
|
||||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||||
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
|
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
|
||||||
@@ -467,6 +476,7 @@ private:
|
|||||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||||
#endif
|
#endif
|
||||||
std::string balance_layout_ = "classic";
|
std::string balance_layout_ = "classic";
|
||||||
|
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||||
bool scanline_enabled_ = true;
|
bool scanline_enabled_ = true;
|
||||||
std::set<std::string> hidden_addresses_;
|
std::set<std::string> hidden_addresses_;
|
||||||
std::set<std::string> favorite_addresses_;
|
std::set<std::string> favorite_addresses_;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/typography.h"
|
#include "../material/typography.h"
|
||||||
#include "../material/project_icons.h"
|
#include "../material/project_icons.h"
|
||||||
|
#include "../notifications.h"
|
||||||
#include "../effects/imgui_acrylic.h"
|
#include "../effects/imgui_acrylic.h"
|
||||||
#include "../effects/scroll_fade_shader.h"
|
#include "../effects/scroll_fade_shader.h"
|
||||||
#include "../effects/low_spec.h"
|
#include "../effects/low_spec.h"
|
||||||
@@ -175,18 +176,6 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|||||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f);
|
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard-grid state for the portfolio cards (drag to move, corner-drag to resize).
|
|
||||||
struct PfCell { int col, row, w, h; };
|
|
||||||
struct PfGridDrag {
|
|
||||||
int drag = -1; // card index being dragged (-1 = none)
|
|
||||||
int resize = -1; // card index being resized
|
|
||||||
bool moved = false; // drag passed the click threshold this gesture
|
|
||||||
ImVec2 press; // mouse pos at grab
|
|
||||||
ImVec2 grab; // mouse offset within the card at grab
|
|
||||||
PfCell old = {0,0,1,1}; // dragged card's cell at grab (for swap-on-drop)
|
|
||||||
PfCell target = {0,0,1,1};// live drop target cell
|
|
||||||
};
|
|
||||||
static PfGridDrag s_grid;
|
|
||||||
|
|
||||||
// Build the right-hand amount strings for a group card / preview from its price-data config.
|
// Build the right-hand amount strings for a group card / preview from its price-data config.
|
||||||
struct PfDisplay {
|
struct PfDisplay {
|
||||||
@@ -326,14 +315,18 @@ static void pfPersist(config::Settings* settings, const std::vector<config::Sett
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Persist the working group when it is named and actually changed; unnamed drafts are dropped.
|
// Persist the working group when it is named and actually changed; unnamed drafts are dropped.
|
||||||
static void pfCommitIfNeeded(config::Settings* settings)
|
// New groups default to the active wallet's scope (per-wallet visibility); existing groups keep
|
||||||
|
// whatever scope they already had.
|
||||||
|
static void pfCommitIfNeeded(App* app)
|
||||||
{
|
{
|
||||||
|
config::Settings* settings = app->settings();
|
||||||
if (s_pfEdit.label[0] == '\0') return;
|
if (s_pfEdit.label[0] == '\0') return;
|
||||||
auto entries = settings->getPortfolioEntries();
|
auto entries = settings->getPortfolioEntries();
|
||||||
bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size());
|
bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size());
|
||||||
config::Settings::PortfolioEntry base;
|
config::Settings::PortfolioEntry base;
|
||||||
if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; }
|
if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; }
|
||||||
auto e = pfBuildWorking(base);
|
auto e = pfBuildWorking(base);
|
||||||
|
if (!existing) e.scope = app->activeWalletIdentityHash();
|
||||||
if (existing) entries[s_pfEdit.sel] = e;
|
if (existing) entries[s_pfEdit.sel] = e;
|
||||||
else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; }
|
else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; }
|
||||||
pfPersist(settings, entries);
|
pfPersist(settings, entries);
|
||||||
@@ -830,6 +823,7 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
auto es = settings->getPortfolioEntries();
|
auto es = settings->getPortfolioEntries();
|
||||||
config::Settings::PortfolioEntry ne;
|
config::Settings::PortfolioEntry ne;
|
||||||
ne.label = TR("portfolio_untitled");
|
ne.label = TR("portfolio_untitled");
|
||||||
|
ne.scope = app->activeWalletIdentityHash(); // new group defaults to this wallet
|
||||||
es.push_back(ne);
|
es.push_back(ne);
|
||||||
pfPersist(settings, es);
|
pfPersist(settings, es);
|
||||||
s_pfEdit.sel = (int)es.size() - 1;
|
s_pfEdit.sel = (int)es.size() - 1;
|
||||||
@@ -956,7 +950,7 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry
|
PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
ImGui::BeginDisabled(s_pfEdit.label[0] == '\0');
|
ImGui::BeginDisabled(s_pfEdit.label[0] == '\0');
|
||||||
if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(settings);
|
if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(app);
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
@@ -967,7 +961,7 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
{
|
{
|
||||||
float bh = 40.0f * dp, bw = 120.0f * dp;
|
float bh = 40.0f * dp, bw = 120.0f * dp;
|
||||||
material::RightAlignX(bw);
|
material::RightAlignX(bw);
|
||||||
if (material::TactileButton(TR("portfolio_close"), ImVec2(bw, bh))) { pfCommitIfNeeded(settings); s_pfEdit.open = false; }
|
if (material::TactileButton(TR("portfolio_close"), ImVec2(bw, bh))) { pfCommitIfNeeded(app); s_pfEdit.open = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
material::EndOverlayDialog();
|
material::EndOverlayDialog();
|
||||||
@@ -975,157 +969,95 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
|
|
||||||
bool PortfolioEditorActive() { return s_pfEdit.open; }
|
bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||||
|
|
||||||
// Resolve each portfolio group's grid cell: honor a stored placement when it fits, else auto-place
|
|
||||||
// row-major into the first free cells. Pure + deterministic (no ImGui). One PfCell per entry.
|
|
||||||
static std::vector<PfCell> pfComputeGridLayout(
|
|
||||||
const std::vector<config::Settings::PortfolioEntry>& entries, int cols, int minW, int minH)
|
|
||||||
{
|
|
||||||
int n = (int)entries.size();
|
|
||||||
std::vector<PfCell> layout((size_t)std::max(0, n));
|
|
||||||
std::vector<std::vector<char>> occ;
|
|
||||||
auto ensure = [&](int rEnd) { while ((int)occ.size() < rEnd) occ.emplace_back(cols, 0); };
|
|
||||||
auto fits = [&](int c, int r, int w, int h) -> bool {
|
|
||||||
if (c < 0 || r < 0 || c + w > cols) return false;
|
|
||||||
ensure(r + h);
|
|
||||||
for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) if (occ[rr][cc]) return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
auto mark = [&](int c, int r, int w, int h) { ensure(r + h);
|
|
||||||
for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) occ[rr][cc] = 1; };
|
|
||||||
std::vector<char> placed((size_t)std::max(0, n), 0);
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
const auto& e = entries[i];
|
|
||||||
int w = std::max(minW, std::min(e.gridW, cols)), h = std::max(minH, e.gridH);
|
|
||||||
if (e.gridCol >= 0 && e.gridRow >= 0 && fits(e.gridCol, e.gridRow, w, h)) {
|
|
||||||
layout[i] = {e.gridCol, e.gridRow, w, h}; mark(e.gridCol, e.gridRow, w, h); placed[i] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
if (placed[i]) continue;
|
|
||||||
const auto& e = entries[i];
|
|
||||||
int w = std::max(minW, std::min(e.gridW, cols)), h = std::max(minH, e.gridH);
|
|
||||||
int fc = 0, fr = 0; bool found = false;
|
|
||||||
for (int r = 0; !found && r < 4096; r++)
|
|
||||||
for (int c = 0; c + w <= cols; c++)
|
|
||||||
if (fits(c, r, w, h)) { fc = c; fr = r; found = true; break; }
|
|
||||||
layout[i] = {fc, fr, w, h}; mark(fc, fr, w, h);
|
|
||||||
}
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared drawing context for a portfolio group card (bound once per frame in RenderMarketTab).
|
// ---- Portfolio ROW layouts (replace the drag/resize grid) --------------------------------------
|
||||||
struct MktCardCtx {
|
// Draw one group as a full-width row card in [rowMin,rowMax]; style 0=single-line, 1=two-line,
|
||||||
ImDrawList* dl;
|
// 2=value-hero. Keeps every field (icon, accent, label, DRGX, value, 24h %, sparkline) — hidden
|
||||||
const WalletState* state;
|
// fields per the entry's config simply don't render. Pure drawing into dl.
|
||||||
const MarketInfo* market;
|
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||||
float mktDp;
|
const config::Settings::PortfolioEntry& e, const WalletState& state,
|
||||||
float pfInset;
|
const MarketInfo& market, int style, float dp,
|
||||||
float pfCardPad;
|
ImFont* sub1, ImFont* body2, ImFont* capFont, bool hov)
|
||||||
ImFont* sub1;
|
|
||||||
ImFont* capFont;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Draw one portfolio group card (name/icon, value + %chg, secondary DRGX, sparkline body) within
|
|
||||||
// [gcMin,gcMax] (inset for gaps). hov = hover/active highlight. Pure drawing into cx.dl.
|
|
||||||
static void mktDrawCard(const MktCardCtx& cx, ImVec2 gcMin, ImVec2 gcMax,
|
|
||||||
const config::Settings::PortfolioEntry& e, bool hov)
|
|
||||||
{
|
{
|
||||||
ImDrawList* dl = cx.dl;
|
|
||||||
const auto& state = *cx.state;
|
|
||||||
const auto& market = *cx.market;
|
|
||||||
float mktDp = cx.mktDp, pfInset = cx.pfInset;
|
|
||||||
ImFont* sub1 = cx.sub1, *capFont = cx.capFont;
|
|
||||||
// The grid is gapless, so inset the card within its cell block for separation.
|
|
||||||
gcMin.x += pfInset; gcMin.y += pfInset; gcMax.x -= pfInset; gcMax.y -= pfInset;
|
|
||||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||||
float pad = cx.pfCardPad;
|
float stripW = (style == 2 ? 5.0f : 3.0f) * dp;
|
||||||
GlassPanelSpec gcGlass; gcGlass.rounding = 10.0f;
|
|
||||||
gcGlass.fillAlpha = hov ? 34 : 22; gcGlass.borderAlpha = 40;
|
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 34 : 22; g.borderAlpha = 40;
|
||||||
DrawGlassPanel(dl, gcMin, gcMax, gcGlass);
|
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||||
// Accented groups get a colored outline around the whole card (configurable opacity);
|
|
||||||
// others get a subtle hover ring.
|
|
||||||
if (accent) {
|
if (accent) {
|
||||||
|
dl->AddRectFilled(rowMin, ImVec2(rowMin.x + stripW, rowMax.y), accent, 10.0f, ImDrawFlags_RoundCornersLeft);
|
||||||
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
|
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
|
||||||
dl->AddRect(gcMin, gcMax, WithAlpha(accent, oa), 10.0f, 0, hov ? 2.5f : 2.0f);
|
dl->AddRect(rowMin, rowMax, WithAlpha(accent, oa), 10.0f, 0, hov ? 2.0f : 1.5f);
|
||||||
} else if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
} else if (hov) {
|
||||||
|
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
||||||
auto fit = [&](std::string s, ImFont* f, float maxW) {
|
|
||||||
bool t = false;
|
|
||||||
while (s.size() > 1 && f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x > maxW) { s.pop_back(); t = true; }
|
|
||||||
if (t) s += "\xE2\x80\xA6";
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Value + DRGX strings per basis.
|
|
||||||
char vb[80]; std::string valueStr; bool hasValue = false;
|
|
||||||
if (e.priceBasis == 0 && market.price_usd > 0) { snprintf(vb, sizeof(vb), "$%.2f", bal * market.price_usd); valueStr = vb; hasValue = true; }
|
|
||||||
else if (e.priceBasis == 1 && market.price_btc > 0) { snprintf(vb, sizeof(vb), "%.8f BTC", bal * market.price_btc); valueStr = vb; hasValue = true; }
|
|
||||||
else if (e.priceBasis == 3 && e.manualPrice > 0.0) { snprintf(vb, sizeof(vb), "%.2f %s", bal * e.manualPrice, e.manualCurrency.c_str()); valueStr = vb; hasValue = true; }
|
|
||||||
char db[48]; snprintf(db, sizeof(db), "%.4f %s", bal, DRAGONX_TICKER);
|
|
||||||
bool heroIsValue = e.showValue && hasValue;
|
|
||||||
std::string heroStr = heroIsValue ? valueStr : (e.showDrgx ? std::string(db) : (hasValue ? valueStr : std::string(db)));
|
|
||||||
std::string footStr = (heroIsValue && e.showDrgx) ? std::string(db) : std::string();
|
|
||||||
|
|
||||||
// Compact stat card: name (+ secondary DRGX) on the left; value with % change stacked
|
|
||||||
// below it in the top-right corner; the sparkline fills the body — so it renders even
|
|
||||||
// on a 2-cell card. Tighter padding on short cards.
|
|
||||||
float cardHeight = gcMax.y - gcMin.y;
|
|
||||||
bool cardShort = cardHeight < 90.0f * mktDp;
|
|
||||||
float hpad = cardShort ? Layout::spacingSm() : pad;
|
|
||||||
float hleft = gcMin.x + hpad, hright = gcMax.x - hpad;
|
|
||||||
float hAvailW = hright - hleft;
|
|
||||||
float topY = gcMin.y + hpad;
|
|
||||||
|
|
||||||
// Right column: value (top-right), % change just below it.
|
|
||||||
float rightBottom = topY;
|
|
||||||
float valLeft = hright;
|
|
||||||
bool showChange = e.show24h && (e.priceBasis == 0 || e.priceBasis == 1) && market.change_24h != 0.0;
|
|
||||||
if (!heroStr.empty()) {
|
|
||||||
ImFont* vf = sub1;
|
|
||||||
std::string vs = fit(heroStr, vf, hAvailW * 0.66f);
|
|
||||||
float vw = vf->CalcTextSizeA(vf->LegacySize, FLT_MAX, 0, vs.c_str()).x;
|
|
||||||
valLeft = hright - vw;
|
|
||||||
dl->AddText(vf, vf->LegacySize, ImVec2(valLeft, topY), OnSurface(), vs.c_str());
|
|
||||||
rightBottom = topY + vf->LegacySize;
|
|
||||||
if (showChange) {
|
|
||||||
char cb[32]; snprintf(cb, sizeof(cb), "%+.2f%%", market.change_24h);
|
|
||||||
float cw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, cb).x;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(hright - cw, rightBottom + 1.0f * mktDp),
|
|
||||||
market.change_24h >= 0 ? Success() : Error(), cb);
|
|
||||||
valLeft = std::min(valLeft, hright - cw);
|
|
||||||
rightBottom += capFont->LegacySize + 1.0f * mktDp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left column: icon + name (larger, top), secondary DRGX amount below (always shown).
|
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||||
float nameX = hleft;
|
auto fit = [&](std::string s, ImFont* f, float maxW){ bool t=false; while (s.size()>1 && tw(f,s)>maxW){ s.pop_back(); t=true; } if (t) s += "\xE2\x80\xA6"; return s; };
|
||||||
|
|
||||||
|
char vb[80]; std::string valStr; bool hasValue=false;
|
||||||
|
if (e.priceBasis==0 && market.price_usd>0){ snprintf(vb,sizeof(vb),"$%.2f",bal*market.price_usd); valStr=vb; hasValue=true; }
|
||||||
|
else if (e.priceBasis==1 && market.price_btc>0){ snprintf(vb,sizeof(vb),"%.8f BTC",bal*market.price_btc); valStr=vb; hasValue=true; }
|
||||||
|
else if (e.priceBasis==3 && e.manualPrice>0.0){ snprintf(vb,sizeof(vb),"%.2f %s",bal*e.manualPrice,e.manualCurrency.c_str()); valStr=vb; hasValue=true; }
|
||||||
|
char db[48]; snprintf(db,sizeof(db),"%.4f %s",bal,DRAGONX_TICKER);
|
||||||
|
std::string drgxStr = e.showDrgx ? std::string(db) : std::string();
|
||||||
|
std::string valueStr = (e.showValue && hasValue) ? valStr : std::string();
|
||||||
|
bool showChange = e.show24h && (e.priceBasis==0||e.priceBasis==1) && market.change_24h!=0.0;
|
||||||
|
std::string chgStr; if (showChange){ char cb[32]; snprintf(cb,sizeof(cb),"%+.2f%%",market.change_24h); chgStr=cb; }
|
||||||
|
ImU32 chgCol = market.change_24h>=0 ? Success() : Error();
|
||||||
|
bool showSpark = e.showSparkline && (e.priceBasis==0||e.priceBasis==1);
|
||||||
|
std::vector<double> spark;
|
||||||
|
if (showSpark){ spark=data::sparklineSeries(market,e.sparklineInterval); if (spark.size()<2) showSpark=false; }
|
||||||
|
ImU32 sparkCol = (!spark.empty() && spark.back()>=spark.front()) ? Success() : Error();
|
||||||
|
|
||||||
|
const float padX = Layout::spacingMd(), padY = Layout::spacingSm(), sp = Layout::spacingMd();
|
||||||
|
float left = rowMin.x + stripW + padX;
|
||||||
|
float right = rowMax.x - padX;
|
||||||
|
float midY = (rowMin.y + rowMax.y) * 0.5f;
|
||||||
|
float iconSz = sub1->LegacySize;
|
||||||
|
|
||||||
|
if (style == 0) {
|
||||||
|
// Single line — icon + label (left); drgx / value / 24h / sparkline right-aligned.
|
||||||
|
float y = midY - sub1->LegacySize * 0.5f, x = left, rx = right;
|
||||||
if (!e.icon.empty()) {
|
if (!e.icon.empty()) {
|
||||||
material::project_icons::drawByName(dl, e.icon.c_str(),
|
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, midY), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||||
ImVec2(nameX + sub1->LegacySize * 0.5f, topY + sub1->LegacySize * 0.5f),
|
x += iconSz + Layout::spacingSm();
|
||||||
accent ? accent : OnSurfaceMedium(), Type().iconSmall(), sub1->LegacySize);
|
|
||||||
nameX += sub1->LegacySize + Layout::spacingSm();
|
|
||||||
}
|
}
|
||||||
float nameMaxW = std::max(24.0f * mktDp, valLeft - Layout::spacingSm() - nameX);
|
if (showSpark){ float sw=110*dp; pfDrawSparkline(dl, ImVec2(rx-sw, rowMin.y+padY), ImVec2(rx, rowMax.y-padY), spark, sparkCol); rx -= sw + sp; }
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(nameX, topY), OnSurfaceMedium(),
|
if (!chgStr.empty()){ float w=tw(capFont,chgStr); dl->AddText(capFont,capFont->LegacySize,ImVec2(rx-w, midY-capFont->LegacySize*0.5f), chgCol, chgStr.c_str()); rx -= w + sp; }
|
||||||
fit(e.label, sub1, nameMaxW).c_str());
|
if (!valueStr.empty()){ float w=tw(sub1,valueStr); dl->AddText(sub1,sub1->LegacySize,ImVec2(rx-w,y),OnSurface(),valueStr.c_str()); rx -= w + sp; }
|
||||||
float leftBottom = topY + sub1->LegacySize;
|
if (!drgxStr.empty()){ float w=tw(capFont,drgxStr); dl->AddText(capFont,capFont->LegacySize,ImVec2(rx-w, midY-capFont->LegacySize*0.5f),OnSurfaceMedium(),drgxStr.c_str()); rx -= w + sp; }
|
||||||
if (!footStr.empty()) {
|
float lblMaxW = std::max(24.0f*dp, rx - x - Layout::spacingSm());
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(nameX, leftBottom + 1.0f * mktDp),
|
dl->AddText(sub1,sub1->LegacySize,ImVec2(x,y),OnSurface(),fit(e.label,sub1,lblMaxW).c_str());
|
||||||
OnSurfaceDisabled(), fit(footStr, capFont, nameMaxW).c_str());
|
} else if (style == 1) {
|
||||||
leftBottom += capFont->LegacySize + 1.0f * mktDp;
|
// Two lines — label + (24h, sparkline) on top; DRGX + value below.
|
||||||
|
float topY = rowMin.y + padY, botY = rowMax.y - padY - capFont->LegacySize, x = left, rx = right;
|
||||||
|
if (!e.icon.empty()) {
|
||||||
|
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||||
|
x += iconSz + Layout::spacingSm();
|
||||||
}
|
}
|
||||||
|
if (showSpark){ float sw=130*dp; pfDrawSparkline(dl, ImVec2(rx-sw, topY), ImVec2(rx, midY), spark, sparkCol); rx -= sw + sp; }
|
||||||
// Body: sparkline fills from below the header to the card bottom.
|
if (!chgStr.empty()){ float w=tw(capFont,chgStr); dl->AddText(capFont,capFont->LegacySize,ImVec2(rx-w,topY),chgCol,chgStr.c_str()); rx -= w + sp; }
|
||||||
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) {
|
dl->AddText(sub1,sub1->LegacySize,ImVec2(x,topY),OnSurface(),fit(e.label,sub1,std::max(24.0f*dp, rx-x)).c_str());
|
||||||
std::vector<double> hh = data::sparklineSeries(market, e.sparklineInterval);
|
if (!drgxStr.empty()) dl->AddText(capFont,capFont->LegacySize,ImVec2(x,botY),OnSurfaceMedium(),drgxStr.c_str());
|
||||||
if (hh.size() >= 2) {
|
if (!valueStr.empty()){ float w=tw(body2,valueStr); dl->AddText(body2,body2->LegacySize,ImVec2(right-w, botY-(body2->LegacySize-capFont->LegacySize)),OnSurface(),valueStr.c_str()); }
|
||||||
float sparkTop = std::max(leftBottom, rightBottom) + Layout::spacingXs();
|
} else {
|
||||||
float sparkBot = gcMax.y - hpad;
|
// Value-hero — label; big value; DRGX · 24h; sparkline fills the right.
|
||||||
if (sparkBot - sparkTop >= 8.0f * mktDp)
|
float topY = rowMin.y + padY, x = left;
|
||||||
pfDrawSparklineFilled(dl, ImVec2(hleft, sparkTop), ImVec2(hright, sparkBot),
|
if (!e.icon.empty()) {
|
||||||
hh, hh.back() >= hh.front() ? Success() : Error());
|
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+capFont->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), capFont->LegacySize);
|
||||||
|
x += capFont->LegacySize + Layout::spacingSm();
|
||||||
}
|
}
|
||||||
|
dl->AddText(capFont,capFont->LegacySize,ImVec2(x,topY),OnSurfaceMedium(),fit(e.label,capFont,right-x-140*dp).c_str());
|
||||||
|
float valY = topY + capFont->LegacySize + Layout::spacingXs();
|
||||||
|
std::string hero = !valueStr.empty()? valueStr : drgxStr;
|
||||||
|
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(left, valY), accent?accent:Success(), hero.c_str());
|
||||||
|
float botY = valY + sub1->LegacySize + Layout::spacingXs();
|
||||||
|
std::string sub = (!valueStr.empty() && !drgxStr.empty()) ? drgxStr : std::string();
|
||||||
|
if (!sub.empty()) dl->AddText(capFont,capFont->LegacySize,ImVec2(left,botY),OnSurfaceDisabled(),sub.c_str());
|
||||||
|
if (!chgStr.empty()){ float sw = sub.empty()?0.0f:(tw(capFont,sub)+tw(capFont," \xC2\xB7 ")); if(!sub.empty()) dl->AddText(capFont,capFont->LegacySize,ImVec2(left+tw(capFont,sub),botY),OnSurfaceDisabled()," \xC2\xB7 "); dl->AddText(capFont,capFont->LegacySize,ImVec2(left+sw,botY),chgCol,chgStr.c_str()); }
|
||||||
|
if (showSpark){ float sw=(right-left)*0.42f; pfDrawSparklineFilled(dl, ImVec2(right-sw, valY-2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,10 +1169,6 @@ struct MktCtx {
|
|||||||
bool chartUp;
|
bool chartUp;
|
||||||
double periodChangePct;
|
double periodChangePct;
|
||||||
std::string periodSuffix;
|
std::string periodSuffix;
|
||||||
const std::vector<config::Settings::PortfolioEntry>* pfEntriesGeo;
|
|
||||||
const std::vector<PfCell>* pfLayout;
|
|
||||||
int pfN, pfCols, pfMinW, pfMinH, pfMaxRow;
|
|
||||||
float pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combined hero card: draws the ONE glass panel spanning the price header AND the chart below it,
|
// Combined hero card: draws the ONE glass panel spanning the price header AND the chart below it,
|
||||||
@@ -1659,10 +1587,6 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
|||||||
ImFont* sub1 = Type().subtitle1();
|
ImFont* sub1 = Type().subtitle1();
|
||||||
ImFont* body2 = Type().body2();
|
ImFont* body2 = Type().body2();
|
||||||
float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH;
|
float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH;
|
||||||
const auto& pfEntriesGeo = *cx.pfEntriesGeo;
|
|
||||||
const auto& pfLayout = *cx.pfLayout;
|
|
||||||
int pfN = cx.pfN, pfCols = cx.pfCols, pfMinW = cx.pfMinW, pfMinH = cx.pfMinH, pfMaxRow = cx.pfMaxRow;
|
|
||||||
float pfCellW = cx.pfCellW, pfCellH = cx.pfCellH, pfInset = cx.pfInset, pfCardPad = cx.pfCardPad, pfCardGap = cx.pfCardGap;
|
|
||||||
char buf[128];
|
char buf[128];
|
||||||
|
|
||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||||
@@ -1760,157 +1684,51 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
|||||||
ImVec2(cx0, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf);
|
ImVec2(cx0, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- CUSTOM GROUPS — dashboard grid: drag a card to move it, drag its corner to resize.
|
// ---- CUSTOM GROUPS — full-width rows in the selected style (single-line / two-line / value-hero),
|
||||||
// A subtle dot grid shows while rearranging. Click (no drag) opens the editor. ----
|
// inside an internal scroll region so the portfolio stays within its height budget. Left/Right
|
||||||
if (pfN > 0) {
|
// arrows switch styles (handled in RenderMarketTab). Click a row to edit it. ----
|
||||||
float gy = cardMin.y + pfSummaryH;
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
bool interacting = (s_grid.drag >= 0 || s_grid.resize >= 0);
|
|
||||||
float rh = 20.0f * mktDp; // corner resize-grip size (generous so a short resize is easy to grab)
|
|
||||||
|
|
||||||
auto cellMin = [&](const PfCell& L) {
|
|
||||||
return ImVec2(cx0 + L.col * (pfCellW + pfCardGap), gy + L.row * (pfCellH + pfCardGap));
|
|
||||||
};
|
|
||||||
auto cellSize = [&](int w, int h) {
|
|
||||||
return ImVec2(w * pfCellW + (w - 1) * pfCardGap, h * pfCellH + (h - 1) * pfCardGap);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Draw one card's content into [gcMin, gcMax].
|
|
||||||
// Stat tile: header (icon + name, 24h badge) / hero value (large, centered band) /
|
|
||||||
// footer (DRGX amount) + a dedicated sparkline strip along the bottom.
|
|
||||||
MktCardCtx cardCx{ dl, &state, &market, mktDp, pfInset, pfCardPad, sub1, capFont };
|
|
||||||
|
|
||||||
// Subtle dot grid while rearranging (fine 32px cells). Fill the whole visible grid area
|
|
||||||
// — down to the bottom of the scroll viewport, not just the occupied rows — so a card can
|
|
||||||
// be dropped anywhere, including the empty space below the current cards.
|
|
||||||
if (interacting) {
|
|
||||||
ImU32 dotc = WithAlpha(OnSurface(), 38);
|
|
||||||
float visibleBottom = dl->GetClipRectMax().y; // bottom edge of the scroll child
|
|
||||||
int rowsToBottom = (int)((visibleBottom - gy) / pfCellH) + 1;
|
|
||||||
int lastRow = std::max(pfMaxRow + 2, rowsToBottom);
|
|
||||||
for (int r = 0; r <= lastRow; r++)
|
|
||||||
for (int c = 0; c <= pfCols; c++)
|
|
||||||
dl->AddCircleFilled(ImVec2(cx0 + c * pfCellW, gy + r * pfCellH), 1.2f * mktDp, dotc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw + hit-test each card. Skip the one being dragged/resized (drawn specially below).
|
|
||||||
for (int i = 0; i < pfN; i++) {
|
|
||||||
ImVec2 gcMin = cellMin(pfLayout[i]);
|
|
||||||
ImVec2 sz = cellSize(pfLayout[i].w, pfLayout[i].h);
|
|
||||||
ImVec2 gcMax(gcMin.x + sz.x, gcMin.y + sz.y);
|
|
||||||
|
|
||||||
if (i == s_grid.drag && s_grid.moved) {
|
|
||||||
dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 45), 10.0f, 0, 1.0f); // source ghost
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (i == s_grid.resize) continue;
|
|
||||||
|
|
||||||
bool hov = !interacting && material::IsRectHovered(gcMin, gcMax);
|
|
||||||
mktDrawCard(cardCx, gcMin, gcMax, pfEntriesGeo[i], hov);
|
|
||||||
|
|
||||||
// Resize grip (bottom-right).
|
|
||||||
bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax);
|
|
||||||
ImU32 gcc = WithAlpha(OnSurface(), gripHov ? 170 : 80);
|
|
||||||
for (int k = 1; k <= 2; k++) {
|
|
||||||
float o = k * 4.0f * mktDp;
|
|
||||||
dl->AddLine(ImVec2(gcMax.x - o, gcMax.y - 3.0f), ImVec2(gcMax.x - 3.0f, gcMax.y - o), gcc, 1.3f * mktDp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a gesture only when idle. One body button; the press location (corner vs body)
|
|
||||||
// decides resize vs move — avoids overlapping-item hit-test issues.
|
|
||||||
if (s_grid.drag < 0 && s_grid.resize < 0) {
|
|
||||||
ImGui::PushID(3000 + i);
|
|
||||||
ImGui::SetCursorScreenPos(gcMin);
|
|
||||||
ImGui::InvisibleButton("##pfBody", sz);
|
|
||||||
bool bodyAct = ImGui::IsItemActivated();
|
|
||||||
bool bodyHov = ImGui::IsItemHovered();
|
|
||||||
ImGui::PopID();
|
|
||||||
if (bodyHov) ImGui::SetMouseCursor(gripHov ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_Hand);
|
|
||||||
if (bodyAct) {
|
|
||||||
if (gripHov) { s_grid.resize = i; s_grid.old = pfLayout[i]; }
|
|
||||||
else {
|
|
||||||
s_grid.drag = i; s_grid.moved = false; s_grid.old = pfLayout[i];
|
|
||||||
s_grid.press = io.MousePos;
|
|
||||||
s_grid.grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto pfPersist = [&](const std::vector<config::Settings::PortfolioEntry>& v) {
|
|
||||||
app->settings()->setPortfolioEntries(v); app->settings()->save();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----- Resize in progress (corner drag) -----
|
|
||||||
if (s_grid.resize >= 0 && s_grid.resize < pfN) {
|
|
||||||
int i = s_grid.resize;
|
|
||||||
ImVec2 mn = cellMin(pfLayout[i]);
|
|
||||||
int nw = std::max(pfMinW, std::min(pfCols - pfLayout[i].col,
|
|
||||||
(int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f)));
|
|
||||||
int nh = std::max(pfMinH, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + pfCardGap) + 0.5f));
|
|
||||||
ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y);
|
|
||||||
mktDrawCard(cardCx, mn, mx, pfEntriesGeo[i], true);
|
|
||||||
dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
|
||||||
// Live size readout (e.g. "8\xC3\x972") so the reached grid span is visible — the
|
|
||||||
// minimum is pfMinW x pfMinH (8x2).
|
|
||||||
{
|
{
|
||||||
char szb[32]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh);
|
const auto& allEntries = app->settings()->getPortfolioEntries();
|
||||||
ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb);
|
const std::string activeHash = app->activeWalletIdentityHash();
|
||||||
ImVec2 tp((mn.x + mx.x) * 0.5f - tsz.x * 0.5f, (mn.y + mx.y) * 0.5f - tsz.y * 0.5f);
|
// Visible = this wallet's groups + legacy (empty scope). Keep storage indices for edit targets.
|
||||||
dl->AddRectFilled(ImVec2(tp.x - 6, tp.y - 3), ImVec2(tp.x + tsz.x + 6, tp.y + tsz.y + 3),
|
std::vector<int> vis;
|
||||||
IM_COL32(20, 20, 30, 205), 4.0f);
|
for (int i = 0; i < (int)allEntries.size(); i++) {
|
||||||
dl->AddText(sub1, sub1->LegacySize, tp, OnSurface(), szb);
|
const auto& e = allEntries[i];
|
||||||
}
|
if (e.scope.empty() || activeHash.empty() || e.scope == activeHash) vis.push_back(i);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE);
|
|
||||||
if (!io.MouseDown[0]) {
|
|
||||||
auto entries = app->settings()->getPortfolioEntries();
|
|
||||||
if (i < (int)entries.size()) {
|
|
||||||
entries[i].gridCol = pfLayout[i].col; entries[i].gridRow = pfLayout[i].row;
|
|
||||||
entries[i].gridW = nw; entries[i].gridH = nh;
|
|
||||||
pfPersist(entries);
|
|
||||||
}
|
|
||||||
s_grid.resize = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Drag in progress -----
|
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||||
if (s_grid.drag >= 0 && s_grid.drag < pfN) {
|
float rowH = (style == 0 ? 46.0f : style == 1 ? 64.0f : 84.0f) * mktDp;
|
||||||
int i = s_grid.drag;
|
float rowGap = Layout::spacingSm();
|
||||||
if (io.MouseDown[0]) {
|
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
||||||
if (std::abs(io.MousePos.x - s_grid.press.x) > 4.0f || std::abs(io.MousePos.y - s_grid.press.y) > 4.0f)
|
|
||||||
s_grid.moved = true;
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
||||||
if (s_grid.moved) {
|
// Flush-left child (no window padding) so rows align with the summary; a scrollbar appears
|
||||||
int w = pfLayout[i].w, h = pfLayout[i].h;
|
// only when the visible groups overflow the bounded height.
|
||||||
float tx = io.MousePos.x - s_grid.grab.x, ty = io.MousePos.y - s_grid.grab.y;
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||||
int tc = std::max(0, std::min(pfCols - w, (int)((tx - cx0) / (pfCellW + pfCardGap) + 0.5f)));
|
ImGui::BeginChild("##pfRows", ImVec2(availWidth, rowsH), false);
|
||||||
int tr = std::max(0, (int)((ty - gy) / (pfCellH + pfCardGap) + 0.5f));
|
ImGui::PopStyleVar();
|
||||||
s_grid.target = {tc, tr, w, h};
|
ImDrawList* rdl = ImGui::GetWindowDrawList();
|
||||||
ImVec2 tmn(cx0 + tc * (pfCellW + pfCardGap), gy + tr * (pfCellH + pfCardGap));
|
float rowW = ImGui::GetContentRegionAvail().x; // narrows automatically if a scrollbar shows
|
||||||
ImVec2 tsz = cellSize(w, h), tmx(tmn.x + tsz.x, tmn.y + tsz.y);
|
if (vis.empty()) {
|
||||||
dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f);
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
dl->AddRect(tmn, tmx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||||
ImVec2 fmn(tx, ty), fmx(fmn.x + tsz.x, fmn.y + tsz.y);
|
|
||||||
mktDrawCard(cardCx, fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!s_grid.moved) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||||
else {
|
int i = vis[vi];
|
||||||
auto entries = app->settings()->getPortfolioEntries();
|
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||||
if (i < (int)entries.size()) {
|
ImVec2 rMax(rMin.x + rowW, rMin.y + rowH);
|
||||||
int occ = -1;
|
ImGui::PushID(4000 + i);
|
||||||
for (int j = 0; j < pfN; j++)
|
bool clicked = ImGui::InvisibleButton("##pfRowBtn", ImVec2(rowW, rowH));
|
||||||
if (j != i && pfLayout[j].col == s_grid.target.col && pfLayout[j].row == s_grid.target.row) { occ = j; break; }
|
bool hov = ImGui::IsItemHovered();
|
||||||
entries[i].gridCol = s_grid.target.col; entries[i].gridRow = s_grid.target.row;
|
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (occ >= 0 && occ < (int)entries.size()) {
|
ImGui::PopID();
|
||||||
entries[occ].gridCol = s_grid.old.col; entries[occ].gridRow = s_grid.old.row;
|
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, body2, capFont, hov);
|
||||||
}
|
if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
||||||
pfPersist(entries);
|
if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap));
|
||||||
}
|
|
||||||
}
|
|
||||||
s_grid.drag = -1; s_grid.moved = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH));
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH));
|
||||||
@@ -1949,6 +1767,22 @@ void RenderMarketTab(App* app)
|
|||||||
const auto& currentExchange = registry[s_mkt.exchangeIdx];
|
const auto& currentExchange = registry[s_mkt.exchangeIdx];
|
||||||
if (s_mkt.pairIdx >= (int)currentExchange.pairs.size()) s_mkt.pairIdx = 0;
|
if (s_mkt.pairIdx >= (int)currentExchange.pairs.size()) s_mkt.pairIdx = 0;
|
||||||
|
|
||||||
|
// Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the
|
||||||
|
// Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the
|
||||||
|
// portfolio editor modal is open.
|
||||||
|
if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||||
|
bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
|
||||||
|
bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
|
||||||
|
if (prev || next) {
|
||||||
|
int st = app->settings()->getPortfolioStyle();
|
||||||
|
st = next ? (st + 1) % 3 : (st + 2) % 3;
|
||||||
|
app->settings()->setPortfolioStyle(st);
|
||||||
|
const char* names[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||||
|
TR("portfolio_style_featured") };
|
||||||
|
Notifications::instance().info(std::string(TR("portfolio_style_label")) + ": " + names[st]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// (Panel theme-effects are suppressed frame-wide in App::render() while the modal is open, so
|
// (Panel theme-effects are suppressed frame-wide in App::render() while the modal is open, so
|
||||||
// their foreground borders don't bleed over the overlay — see PortfolioEditorActive().)
|
// their foreground borders don't bleed over the overlay — see PortfolioEditorActive().)
|
||||||
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
|
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
|
||||||
@@ -1991,23 +1825,20 @@ void RenderMarketTab(App* app)
|
|||||||
+ body2->LegacySize + Layout::spacingSm() // DRGX balance row
|
+ body2->LegacySize + Layout::spacingSm() // DRGX balance row
|
||||||
+ ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label
|
+ ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label
|
||||||
+ Layout::spacingMd(); // gap before the group cards
|
+ Layout::spacingMd(); // gap before the group cards
|
||||||
int pfN = (int)pfEntriesGeo.size();
|
// Content height = full-width rows in the selected style, confined to a bounded area (up to
|
||||||
float pfCardPad = Layout::spacingMd(); // inner padding of each group card
|
// pfMaxVisibleRows) with an internal scroll when the visible groups overflow. Only groups
|
||||||
// Fine SQUARE grid: cells are ~32px minimum and scale up to evenly fill the width. Cards snap to
|
// visible in the active wallet (own scope or legacy/global) count toward the height.
|
||||||
// this grid and span many cells; they're drawn with an inner inset so adjacent cards don't touch.
|
const std::string pfActiveHash = app->activeWalletIdentityHash();
|
||||||
float pfCardGap = 0.0f; // gapless cell grid
|
int pfVisN = 0;
|
||||||
float pfInset = 3.0f * mktDp; // per-card inset for visual separation
|
for (const auto& e : pfEntriesGeo)
|
||||||
const int pfMinW = 8, pfMinH = 2; // minimum card size in cells (~256x64)
|
if (e.scope.empty() || pfActiveHash.empty() || e.scope == pfActiveHash) pfVisN++;
|
||||||
int pfCols = std::max(pfMinW, (int)(availWidth / (32.0f * mktDp)));
|
int pfStyle = app->settings()->getPortfolioStyle();
|
||||||
float pfCellW = availWidth / (float)pfCols; // square cell edge (>= ~32px)
|
float pfRowH = (pfStyle == 0 ? 46.0f : pfStyle == 1 ? 64.0f : 84.0f) * mktDp;
|
||||||
float pfCellH = pfCellW;
|
float pfRowGap = Layout::spacingSm();
|
||||||
// Resolve each group's grid cell: honor a stored placement when it fits, else auto-place
|
const int pfMaxVisibleRows = 4; // rows shown before the list scrolls
|
||||||
// row-major into the first free cells. Deterministic each frame.
|
float pfWantH = (pfVisN > 0) ? (pfVisN * pfRowH + std::max(0, pfVisN - 1) * pfRowGap) : 0.0f;
|
||||||
std::vector<PfCell> pfLayout = pfComputeGridLayout(pfEntriesGeo, pfCols, pfMinW, pfMinH);
|
float pfCapH = pfMaxVisibleRows * pfRowH + (pfMaxVisibleRows - 1) * pfRowGap;
|
||||||
int pfMaxRow = 0; for (const auto& L : pfLayout) pfMaxRow = std::max(pfMaxRow, L.row + L.h);
|
float pfGroupsH = std::min(pfWantH, pfCapH);
|
||||||
// Content height = the grid plus a couple of empty rows so cards can be dragged below the
|
|
||||||
// current bottom (the tab scrolls when the total exceeds the window).
|
|
||||||
float pfGroupsH = (pfN > 0) ? ((pfMaxRow + 2) * pfCellH) : 0.0f;
|
|
||||||
float portfolioH = pfSummaryH + pfGroupsH;
|
float portfolioH = pfSummaryH + pfGroupsH;
|
||||||
|
|
||||||
// Chart series for the selected interval (timestamped), shared by the hero change badge and
|
// Chart series for the selected interval (timestamped), shared by the hero change badge and
|
||||||
@@ -2040,9 +1871,7 @@ void RenderMarketTab(App* app)
|
|||||||
MktCtx mktc{
|
MktCtx mktc{
|
||||||
app, ¤tExchange,
|
app, ¤tExchange,
|
||||||
chartH, heroHeaderH, pfSummaryH, portfolioH, ratioBarH,
|
chartH, heroHeaderH, pfSummaryH, portfolioH, ratioBarH,
|
||||||
&chartTimes, nowSec, chartUp, periodChangePct, periodSuffix,
|
&chartTimes, nowSec, chartUp, periodChangePct, periodSuffix
|
||||||
&pfEntriesGeo, &pfLayout, pfN, pfCols, pfMinW, pfMinH, pfMaxRow,
|
|
||||||
pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap
|
|
||||||
};
|
};
|
||||||
mktDrawPriceHero(mktc);
|
mktDrawPriceHero(mktc);
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ public:
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (StyledButton(TR("refresh"), ImVec2(110.0f * dp, 0))) s_needScan = true;
|
if (StyledButton(TR("refresh"), ImVec2(110.0f * dp, 0))) s_needScan = true;
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
if (StyledButton(TR("wallets_reveal"), ImVec2(150.0f * dp, 0)))
|
||||||
|
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
||||||
|
ImGui::SameLine();
|
||||||
if (StyledButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false;
|
if (StyledButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false;
|
||||||
|
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["wallets_imported"] = "Wallet imported — switching…";
|
strings_["wallets_imported"] = "Wallet imported — switching…";
|
||||||
strings_["wallets_import_failed"] = "Could not import that wallet file.";
|
strings_["wallets_import_failed"] = "Could not import that wallet file.";
|
||||||
strings_["wallets_creating"] = "Creating wallet — the node will restart…";
|
strings_["wallets_creating"] = "Creating wallet — the node will restart…";
|
||||||
|
strings_["wallets_reveal"] = "Reveal folder";
|
||||||
strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it";
|
strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it";
|
||||||
// Migrate-to-seed modal
|
// Migrate-to-seed modal
|
||||||
strings_["mig_title"] = "Migrate to a seed wallet";
|
strings_["mig_title"] = "Migrate to a seed wallet";
|
||||||
@@ -1300,6 +1301,10 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["portfolio_all_transparent"] = "All transparent";
|
strings_["portfolio_all_transparent"] = "All transparent";
|
||||||
strings_["portfolio_clear_sel"] = "Clear";
|
strings_["portfolio_clear_sel"] = "Clear";
|
||||||
strings_["portfolio_no_entries"] = "No custom entries yet. Add one to track a group of addresses.";
|
strings_["portfolio_no_entries"] = "No custom entries yet. Add one to track a group of addresses.";
|
||||||
|
strings_["portfolio_style_label"] = "Portfolio style";
|
||||||
|
strings_["portfolio_style_compact"] = "Compact rows";
|
||||||
|
strings_["portfolio_style_detailed"] = "Detailed rows";
|
||||||
|
strings_["portfolio_style_featured"] = "Featured rows";
|
||||||
strings_["portfolio_edit"] = "Edit";
|
strings_["portfolio_edit"] = "Edit";
|
||||||
strings_["portfolio_delete"] = "Delete";
|
strings_["portfolio_delete"] = "Delete";
|
||||||
strings_["portfolio_save"] = "Save";
|
strings_["portfolio_save"] = "Save";
|
||||||
|
|||||||
Reference in New Issue
Block a user