diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 1b6813d..e037cda 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -318,6 +318,18 @@ bool Settings::load(const std::string& path) entry.icon = e["icon"].get(); if (e.contains("color") && e["color"].is_number()) entry.color = e["color"].get(); + if (e.contains("price_basis") && e["price_basis"].is_number_integer()) + entry.priceBasis = e["price_basis"].get(); + if (e.contains("manual_price") && e["manual_price"].is_number()) + entry.manualPrice = e["manual_price"].get(); + if (e.contains("manual_currency") && e["manual_currency"].is_string()) + entry.manualCurrency = e["manual_currency"].get(); + if (e.contains("show_drgx") && e["show_drgx"].is_boolean()) + entry.showDrgx = e["show_drgx"].get(); + if (e.contains("show_value") && e["show_value"].is_boolean()) + entry.showValue = e["show_value"].get(); + if (e.contains("show_24h") && e["show_24h"].is_boolean()) + entry.show24h = e["show_24h"].get(); if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry)); } } @@ -469,6 +481,12 @@ bool Settings::save(const std::string& path) for (const auto& a : e.addresses) entry["addresses"].push_back(a); entry["icon"] = e.icon; entry["color"] = e.color; + entry["price_basis"] = e.priceBasis; + entry["manual_price"] = e.manualPrice; + entry["manual_currency"] = e.manualCurrency; + entry["show_drgx"] = e.showDrgx; + entry["show_value"] = e.showValue; + entry["show_24h"] = e.show24h; j["portfolio_entries"].push_back(std::move(entry)); } j["font_scale"] = font_scale_; diff --git a/src/config/settings.h b/src/config/settings.h index 30e88f3..0f3dba5 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -80,6 +80,15 @@ public: std::vector addresses; std::string icon; // project_icons wallet-icon name; empty = no icon unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default + // Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC), + // 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior + // (show DRGX + USD value). + int priceBasis = 0; + double manualPrice = 0.0; // price per DRGX for the Manual basis + std::string manualCurrency = "USD"; + bool showDrgx = true; // show the DRGX amount on the card + bool showValue = true; // show the converted/fiat value on the card + bool show24h = false; // show the 24h % change (live-market bases only) }; // Theme diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 2403f70..e72c80c 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -103,6 +103,50 @@ static char s_pf_search[64] = {0}; // address-list filter text static int s_pf_type_filter = 0; // 0 = all, 1 = shielded, 2 = transparent static bool s_pf_funded_only = false; static float s_pf_custom_rgb[3] = {0.31f, 0.62f, 1.0f}; // working RGB for the custom color picker +// Price-data config (working copy while editing a group). +static int s_pf_price_basis = 0; +static double s_pf_manual_price = 0.0; +static char s_pf_manual_ccy[12] = "USD"; +static bool s_pf_show_drgx = true; +static bool s_pf_show_value = true; +static bool s_pf_show_24h = false; + +// Build the right-hand amount strings for a group card / preview from its price-data config. +struct PfDisplay { + std::string primary; // name-row, top-right (DRGX amount or the value) + std::string secondary; // below primary (the value, when DRGX is also shown) + std::string change; // "+2.34%" (live-market bases only) or empty + ImU32 changeCol = 0; +}; +static PfDisplay pfBuildDisplay(const config::Settings::PortfolioEntry& e, double bal, + const MarketInfo& market) +{ + PfDisplay d; + char b[80]; + std::string valueStr; bool hasValue = false; + if (e.priceBasis == 0 && market.price_usd > 0) { + snprintf(b, sizeof(b), "$%.2f", bal * market.price_usd); valueStr = b; hasValue = true; + } else if (e.priceBasis == 1 && market.price_btc > 0) { + snprintf(b, sizeof(b), "%.8f BTC", bal * market.price_btc); valueStr = b; hasValue = true; + } else if (e.priceBasis == 3 && e.manualPrice > 0.0) { + snprintf(b, sizeof(b), "%.2f %s", bal * e.manualPrice, e.manualCurrency.c_str()); + valueStr = b; hasValue = true; + } + std::string drgxStr; + if (e.showDrgx) { snprintf(b, sizeof(b), "%.4f %s", bal, DRAGONX_TICKER); drgxStr = b; } + if (e.showDrgx) { + d.primary = drgxStr; + if (e.showValue && hasValue) d.secondary = valueStr; + } else if (e.showValue && hasValue) { + d.primary = valueStr; + } + if (e.show24h && (e.priceBasis == 0 || e.priceBasis == 1) && market.change_24h != 0.0) { + snprintf(b, sizeof(b), "%+.2f%%", market.change_24h); + d.change = b; + d.changeCol = market.change_24h >= 0 ? material::Success() : material::Error(); + } + return d; +} // Preset accent palette for portfolio groups (0 slot = "default / no color"). static const ImU32 kPfPalette[] = { @@ -122,15 +166,28 @@ static void PortfolioBeginEdit(App* app, int index) const auto& entries = app->settings()->getPortfolioEntries(); s_pf_editing = index; if (index >= 0 && index < (int)entries.size()) { - snprintf(s_pf_label, sizeof(s_pf_label), "%s", entries[index].label.c_str()); - s_pf_addrs = entries[index].addresses; - s_pf_icon = entries[index].icon; - s_pf_color = entries[index].color; + const auto& src = entries[index]; + snprintf(s_pf_label, sizeof(s_pf_label), "%s", src.label.c_str()); + s_pf_addrs = src.addresses; + s_pf_icon = src.icon; + s_pf_color = src.color; + s_pf_price_basis = src.priceBasis; + s_pf_manual_price = src.manualPrice; + snprintf(s_pf_manual_ccy, sizeof(s_pf_manual_ccy), "%s", src.manualCurrency.c_str()); + s_pf_show_drgx = src.showDrgx; + s_pf_show_value = src.showValue; + s_pf_show_24h = src.show24h; } else { s_pf_label[0] = '\0'; s_pf_addrs.clear(); s_pf_icon.clear(); s_pf_color = 0; + s_pf_price_basis = 0; + s_pf_manual_price = 0.0; + snprintf(s_pf_manual_ccy, sizeof(s_pf_manual_ccy), "USD"); + s_pf_show_drgx = true; + s_pf_show_value = true; + s_pf_show_24h = false; } s_pf_search[0] = '\0'; s_pf_type_filter = 0; @@ -234,18 +291,32 @@ static void RenderPortfolioEditor(App* app) pdl->AddText(sub1f, sub1f->LegacySize, ImVec2(nameX, rowY), s_pf_label[0] ? OnSurface() : OnSurfaceDisabled(), nm); double pbal = data::SumPortfolioBalance(s_pf_addrs, state.addresses); - char pbuf[64]; - snprintf(pbuf, sizeof(pbuf), "%.4f %s", pbal, DRAGONX_TICKER); - float pbw = body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, pbuf).x; - pdl->AddText(body2f, body2f->LegacySize, - ImVec2(pMax.x - ppad - pbw, rowY + (sub1f->LegacySize - body2f->LegacySize) * 0.5f), - OnSurface(), pbuf); - if (state.market.price_usd > 0) { - snprintf(pbuf, sizeof(pbuf), "$%.2f", pbal * state.market.price_usd); - float uw = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pbuf).x; - pdl->AddText(capF, capF->LegacySize, - ImVec2(pMax.x - ppad - uw, rowY + sub1f->LegacySize + Layout::spacingXs()), - OnSurfaceDisabled(), pbuf); + config::Settings::PortfolioEntry tmp; + tmp.priceBasis = s_pf_price_basis; + tmp.manualPrice = s_pf_manual_price; + tmp.manualCurrency = s_pf_manual_ccy; + tmp.showDrgx = s_pf_show_drgx; + tmp.showValue = s_pf_show_value; + tmp.show24h = s_pf_show_24h; + PfDisplay pd = pfBuildDisplay(tmp, pbal, state.market); + float primW = pd.primary.empty() ? 0.0f + : body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, pd.primary.c_str()).x; + if (!pd.primary.empty()) + pdl->AddText(body2f, body2f->LegacySize, + ImVec2(pMax.x - ppad - primW, rowY + (sub1f->LegacySize - body2f->LegacySize) * 0.5f), + OnSurface(), pd.primary.c_str()); + { + float belowY = rowY + sub1f->LegacySize + Layout::spacingXs(); + float cursorR = pMax.x - ppad; + if (!pd.change.empty()) { + float cw = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pd.change.c_str()).x; + pdl->AddText(capF, capF->LegacySize, ImVec2(cursorR - cw, belowY), pd.changeCol, pd.change.c_str()); + cursorR -= cw + Layout::spacingSm(); + } + if (!pd.secondary.empty()) { + float sw2 = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pd.secondary.c_str()).x; + pdl->AddText(capF, capF->LegacySize, ImVec2(cursorR - sw2, belowY), OnSurfaceDisabled(), pd.secondary.c_str()); + } } ImGui::Dummy(ImVec2(pw, ph)); } @@ -319,6 +390,37 @@ static void RenderPortfolioEditor(App* app) } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + // Price data: how this group's value is priced + which fields the card shows. + { + ImGui::TextUnformatted(TR("portfolio_price")); + const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"), + TR("portfolio_price_drgx"), TR("portfolio_price_manual") }; + ImGui::SetNextItemWidth(-1); + ImGui::Combo("##pfBasis", &s_pf_price_basis, basisItems, 4); + + if (s_pf_price_basis == 3) { // Manual: price-per-DRGX + currency label + float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f; + ImGui::SetNextItemWidth(half); + ImGui::InputDouble("##pfManPrice", &s_pf_manual_price, 0.0, 0.0, "%.6f"); + if (s_pf_manual_price < 0.0) s_pf_manual_price = 0.0; + ImGui::SameLine(); + ImGui::SetNextItemWidth(-1); + ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pf_manual_ccy, sizeof(s_pf_manual_ccy)); + } + + // Field toggles (which price fields appear on the card). + ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pf_show_drgx); + ImGui::SameLine(); + ImGui::BeginDisabled(s_pf_price_basis == 2); // DRGX-only -> no value field + ImGui::Checkbox(TR("portfolio_show_value"), &s_pf_show_value); + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); // 24h: live-market only + ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h); + ImGui::EndDisabled(); + } + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + // Icon picker — fills the remaining height of the left column. { float cell = 28.0f * dp, cellGap = 4.0f * dp; @@ -521,6 +623,12 @@ static void RenderPortfolioEditor(App* app) e.addresses = s_pf_addrs; e.icon = s_pf_icon; e.color = s_pf_color; + e.priceBasis = s_pf_price_basis; + e.manualPrice = s_pf_manual_price; + e.manualCurrency = s_pf_manual_ccy; + e.showDrgx = s_pf_show_drgx; + e.showValue = s_pf_show_value; + e.show24h = s_pf_show_24h; if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) entries[s_pf_editing] = e; else entries.push_back(e); persist(entries); @@ -1194,19 +1302,27 @@ void RenderMarketTab(App* app) float rowY = gcMin.y + pfCardPad; float gcRight = gcMax.x - pfCardPad; - // Amounts, top-right corner: DRGX on the name row, fiat directly below it. - snprintf(buf, sizeof(buf), "%.4f %s", bal, DRAGONX_TICKER); - float drgxW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf).x; - dl->AddText(body2, body2->LegacySize, - ImVec2(gcRight - drgxW, rowY + (sub1->LegacySize - body2->LegacySize) * 0.5f), - OnSurface(), buf); - if (market.price_usd > 0) { - char usd[48]; - snprintf(usd, sizeof(usd), "$%.2f", bal * market.price_usd); - float usdW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usd).x; - dl->AddText(capFont, capFont->LegacySize, - ImVec2(gcRight - usdW, rowY + sub1->LegacySize + Layout::spacingXs()), - OnSurfaceDisabled(), usd); + // Amounts, top-right corner, per the group's price-data config. + PfDisplay disp = pfBuildDisplay(e, bal, market); + float primaryW = disp.primary.empty() ? 0.0f + : body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, disp.primary.c_str()).x; + if (!disp.primary.empty()) + dl->AddText(body2, body2->LegacySize, + ImVec2(gcRight - primaryW, rowY + (sub1->LegacySize - body2->LegacySize) * 0.5f), + OnSurface(), disp.primary.c_str()); + { + // Second line: the value (when DRGX is primary) and/or the 24h change. + float belowY = rowY + sub1->LegacySize + Layout::spacingXs(); + float cursorR = gcRight; + if (!disp.change.empty()) { + float cw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, disp.change.c_str()).x; + dl->AddText(capFont, capFont->LegacySize, ImVec2(gcRight - cw, belowY), disp.changeCol, disp.change.c_str()); + cursorR = gcRight - cw - Layout::spacingSm(); + } + if (!disp.secondary.empty()) { + float sw2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, disp.secondary.c_str()).x; + dl->AddText(capFont, capFont->LegacySize, ImVec2(cursorR - sw2, belowY), OnSurfaceDisabled(), disp.secondary.c_str()); + } } // Icon (left) then the group name. @@ -1221,7 +1337,7 @@ void RenderMarketTab(App* app) } // Group name, larger — truncated so it never overlaps the amount. - float nameMaxW = (gcRight - drgxW - Layout::spacingMd()) - nameX; + float nameMaxW = (gcRight - primaryW - Layout::spacingMd()) - nameX; std::string label = e.label; bool truncated = false; while (label.size() > 1 && diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index eefff22..2224163 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1124,6 +1124,16 @@ void I18n::loadBuiltinEnglish() strings_["portfolio_color"] = "Color"; strings_["portfolio_no_icon"] = "None"; strings_["portfolio_custom_color"] = "Custom color\xE2\x80\xA6"; + strings_["portfolio_price"] = "Price"; + strings_["portfolio_price_usd"] = "Market \xC2\xB7 USD"; + strings_["portfolio_price_btc"] = "Market \xC2\xB7 BTC"; + strings_["portfolio_price_drgx"] = "DRGX only"; + strings_["portfolio_price_manual"] = "Manual"; + strings_["portfolio_manual_price"] = "Price / DRGX"; + strings_["portfolio_currency"] = "Currency"; + strings_["portfolio_show"] = "Show:"; + strings_["portfolio_show_value"] = "Value"; + strings_["portfolio_show_24h"] = "24h"; strings_["portfolio_appearance"] = "APPEARANCE"; strings_["portfolio_addresses_hdr"] = "ADDRESSES"; strings_["portfolio_search"] = "Search addresses\xE2\x80\xA6";