diff --git a/src/config/settings.cpp b/src/config/settings.cpp index e5834dd..1b6813d 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -314,6 +314,10 @@ bool Settings::load(const std::string& path) if (e.contains("addresses") && e["addresses"].is_array()) for (const auto& a : e["addresses"]) if (a.is_string()) entry.addresses.push_back(a.get()); + if (e.contains("icon") && e["icon"].is_string()) + entry.icon = e["icon"].get(); + if (e.contains("color") && e["color"].is_number()) + entry.color = e["color"].get(); if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry)); } } @@ -463,6 +467,8 @@ bool Settings::save(const std::string& path) entry["label"] = e.label; entry["addresses"] = json::array(); for (const auto& a : e.addresses) entry["addresses"].push_back(a); + entry["icon"] = e.icon; + entry["color"] = e.color; 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 8a67369..30e88f3 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -78,6 +78,8 @@ public: struct PortfolioEntry { std::string label; 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 }; // Theme diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index c7f883d..f8b8b6a 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -15,6 +15,7 @@ #include "../material/draw_helpers.h" #include "../material/colors.h" #include "../material/typography.h" +#include "../material/project_icons.h" #include "../../embedded/IconsMaterialDesign.h" #include "../../util/platform.h" #include "../layout.h" @@ -95,6 +96,20 @@ static bool s_portfolio_editor_open = false; static int s_pf_editing = -1; // -1 = list mode; -2 = adding new; >=0 = editing that index static char s_pf_label[64] = {0}; static std::vector s_pf_addrs; +static std::string s_pf_icon; // selected wallet-icon name ("" = none) +static unsigned int s_pf_color = 0; // selected accent (packed IM_COL32; 0 = default) + +// Preset accent palette for portfolio groups (0 slot = "default / no color"). +static const ImU32 kPfPalette[] = { + IM_COL32(0x4F, 0x9D, 0xFF, 0xFF), // blue + IM_COL32(0x3D, 0xDC, 0x84, 0xFF), // green + IM_COL32(0xFF, 0xB0, 0x3D, 0xFF), // amber + IM_COL32(0xFF, 0x6B, 0x6B, 0xFF), // red + IM_COL32(0xB0, 0x7B, 0xFF, 0xFF), // purple + IM_COL32(0x4F, 0xD6, 0xD6, 0xFF), // teal + IM_COL32(0xFF, 0x8F, 0xC7, 0xFF), // pink + IM_COL32(0xC7, 0xCE, 0xD6, 0xFF), // slate +}; // Populate the working buffers for a new (index < 0) or existing entry. static void PortfolioBeginEdit(App* app, int index) @@ -103,10 +118,14 @@ static void PortfolioBeginEdit(App* app, int index) 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_addrs = entries[index].addresses; + s_pf_icon = entries[index].icon; + s_pf_color = entries[index].color; } else { s_pf_label[0] = '\0'; s_pf_addrs.clear(); + s_pf_icon.clear(); + s_pf_color = 0; } } @@ -164,6 +183,82 @@ static void RenderPortfolioEditor(App* app) ImGui::InputText("##pfLabel", s_pf_label, sizeof(s_pf_label)); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + // ---- Icon picker (same wallet icon set as the overview tab) ---- + { + float dp = Layout::dpiScale(); + ImFont* iconFont = Type().iconSmall(); + float iconFsz = iconFont->LegacySize; + float cell = 30.0f * dp, cellGap = 4.0f * dp; + ImGui::TextUnformatted(TR("portfolio_icon")); + ImGui::BeginChild("##pfIconGrid", ImVec2(0, cell * 2.0f + cellGap + Layout::spacingSm() * 2.0f), true); + ImDrawList* gdl = ImGui::GetWindowDrawList(); + float availW = ImGui::GetContentRegionAvail().x; + int cols = std::max(1, (int)((availW + cellGap) / (cell + cellGap))); + int total = material::project_icons::walletIconCount() + 1; // slot 0 = "None" + int col = 0; + for (int k = 0; k < total; k++) { + if (col != 0) ImGui::SameLine(0, cellGap); + ImVec2 mn = ImGui::GetCursorScreenPos(); + ImVec2 mx(mn.x + cell, mn.y + cell); + ImVec2 cc(mn.x + cell * 0.5f, mn.y + cell * 0.5f); + bool hov = ImGui::IsMouseHoveringRect(mn, mx); + bool isNone = (k == 0); + const char* nm = isNone ? "" : material::project_icons::walletIconName(k - 1); + bool sel = isNone ? s_pf_icon.empty() : (s_pf_icon == nm); + if (sel) { + gdl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp); + gdl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp); + } else if (hov) { + gdl->AddRectFilled(mn, mx, IM_COL32(255, 255, 255, 20), 6.0f * dp); + } + ImU32 icol = sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()); + if (isNone) { // slashed circle = "no icon" + float r = cell * 0.26f; + gdl->AddCircle(cc, r, icol, 0, 1.5f * dp); + gdl->AddLine(ImVec2(cc.x - r * 0.7f, cc.y + r * 0.7f), + ImVec2(cc.x + r * 0.7f, cc.y - r * 0.7f), icol, 1.5f * dp); + } else { + material::project_icons::drawByName(gdl, nm, cc, icol, iconFont, iconFsz); + } + ImGui::PushID(k); + ImGui::InvisibleButton("##pfic", ImVec2(cell, cell)); + if (ImGui::IsItemClicked()) s_pf_icon = isNone ? std::string() : std::string(nm); + if (hov) material::Tooltip("%s", isNone ? TR("portfolio_no_icon") : nm); + ImGui::PopID(); + col = (col + 1) % cols; + } + ImGui::EndChild(); + } + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + + // ---- Color picker ---- + { + float dp = Layout::dpiScale(); + float sw = 24.0f * dp; + ImGui::TextUnformatted(TR("portfolio_color")); + ImDrawList* cdl = ImGui::GetWindowDrawList(); + int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0])); + for (int c = 0; c <= nColors; c++) { // slot 0 = "default / no color" + if (c != 0) ImGui::SameLine(0, 6.0f * dp); + ImVec2 mn = ImGui::GetCursorScreenPos(); + ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f); + bool isDefault = (c == 0); + bool sel = isDefault ? (s_pf_color == 0u) : (s_pf_color == kPfPalette[c - 1]); + bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw)); + if (isDefault) + cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp); + else + cdl->AddCircleFilled(cc, sw * 0.40f, kPfPalette[c - 1]); + if (sel) cdl->AddCircle(cc, sw * 0.50f, OnSurface(), 0, 2.0f * dp); + else if (hov) cdl->AddCircle(cc, sw * 0.50f, WithAlpha(OnSurface(), 120), 0, 1.5f * dp); + ImGui::PushID(2000 + c); + ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw)); + if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : kPfPalette[c - 1]; + ImGui::PopID(); + } + } + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + // Quick group selectors. if (ImGui::Button(TR("portfolio_all_shielded"))) { for (const auto& a : state.addresses) @@ -211,6 +306,8 @@ static void RenderPortfolioEditor(App* app) config::Settings::PortfolioEntry e; e.label = s_pf_label; e.addresses = s_pf_addrs; + e.icon = s_pf_icon; + e.color = s_pf_color; if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) entries[s_pf_editing] = e; else entries.push_back(e); persist(entries); @@ -863,12 +960,22 @@ void RenderMarketTab(App* app) int row = i / pfCardsPerRow; ImVec2 gcMin(cx + col * (pfCardW + pfCardGap), gy + row * (pfCardH + pfCardGap)); ImVec2 gcMax(gcMin.x + pfCardW, gcMin.y + pfCardH); + ImU32 accent = e.color ? (ImU32)e.color : 0; + bool hov = material::IsRectHovered(gcMin, gcMax); GlassPanelSpec gcGlass; gcGlass.rounding = 10.0f; - gcGlass.fillAlpha = 22; + gcGlass.fillAlpha = hov ? 34 : 22; gcGlass.borderAlpha = 40; DrawGlassPanel(dl, gcMin, gcMax, gcGlass); + if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f); + + // Left accent bar in the group's color (if one is set). + if (accent) { + float bx = gcMin.x + 2.0f; + dl->AddRectFilled(ImVec2(bx, gcMin.y + pfCardPad * 0.6f), + ImVec2(bx + 3.0f, gcMax.y - pfCardPad * 0.6f), accent, 2.0f); + } double bal = data::SumPortfolioBalance(e.addresses, state.addresses); float rowY = gcMin.y + pfCardPad; @@ -889,8 +996,19 @@ void RenderMarketTab(App* app) OnSurfaceDisabled(), usd); } - // Group name, top-left, larger — truncated so it never overlaps the amount. - float nameMaxW = (gcRight - drgxW - Layout::spacingMd()) - (gcMin.x + pfCardPad); + // Icon (left) then the group name. + float nameX = gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f); + if (!e.icon.empty()) { + ImFont* gIcon = Type().iconSmall(); + float gIconSz = sub1->LegacySize; + material::project_icons::drawByName( + dl, e.icon.c_str(), ImVec2(nameX + gIconSz * 0.5f, rowY + sub1->LegacySize * 0.5f), + accent ? accent : OnSurface(), gIcon, gIconSz); + nameX += gIconSz + Layout::spacingSm(); + } + + // Group name, larger — truncated so it never overlaps the amount. + float nameMaxW = (gcRight - drgxW - Layout::spacingMd()) - nameX; std::string label = e.label; bool truncated = false; while (label.size() > 1 && @@ -899,7 +1017,15 @@ void RenderMarketTab(App* app) truncated = true; } if (truncated) label += "\xE2\x80\xA6"; - dl->AddText(sub1, sub1->LegacySize, ImVec2(gcMin.x + pfCardPad, rowY), OnSurface(), label.c_str()); + dl->AddText(sub1, sub1->LegacySize, ImVec2(nameX, rowY), OnSurface(), label.c_str()); + + // Whole card is clickable → open the editor for this group. + ImGui::PushID(3000 + i); + ImGui::SetCursorScreenPos(gcMin); + ImGui::InvisibleButton("##pfCard", ImVec2(pfCardW, pfCardH)); + if (ImGui::IsItemClicked()) { PortfolioBeginEdit(app, i); s_portfolio_editor_open = true; } + if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::PopID(); } } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 97484d4..74a7b85 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1117,9 +1117,12 @@ void I18n::loadBuiltinEnglish() strings_["market_no_price"] = "No price data"; strings_["market_now"] = "Now"; strings_["market_pct_shielded"] = "%.0f%% Shielded"; - strings_["market_portfolio"] = "MY DRGX"; + strings_["market_portfolio"] = "PORTFOLIO"; strings_["portfolio_all_funds"] = "All funds"; strings_["portfolio_manage"] = "Manage\xE2\x80\xA6"; + strings_["portfolio_icon"] = "Icon"; + strings_["portfolio_color"] = "Color"; + strings_["portfolio_no_icon"] = "None"; strings_["portfolio_manage_title"] = "Manage portfolio"; strings_["portfolio_add_entry"] = "Add entry"; strings_["portfolio_new_entry"] = "New entry";