mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 17:01:02 +00:00
578 lines
22 KiB
C++
578 lines
22 KiB
C++
#pragma once
|
|
|
|
// BaseSelectedCardPanel<TCard>
|
|
//
|
|
// Header-only template for the right-hand-side card detail panel:
|
|
//
|
|
// - top: external preview image fetched by `CardPreviewService`
|
|
// - middle: 2-column "label | value" detail grid (Name / Set / ...)
|
|
// - flag-icon row (collapses to nothing when no flags are set)
|
|
// - bottom: "Image N" list box with double-click viewer
|
|
//
|
|
// All of the threading/cancellation machinery for the preview fetch is here
|
|
// (the `shared_ptr<State>` + `std::atomic alive` / `currentGen` pattern from
|
|
// `ui_wx/AGENTS.md`). Subclasses just describe which detail rows to show, the
|
|
// flag-icon strip, and how to extract `(name, setId, setNo)` for the preview
|
|
// lookup key.
|
|
//
|
|
// New games extend this template — see `MagicSelectedCardPanel` and
|
|
// `PokemonSelectedCardPanel` for the canonical patterns.
|
|
|
|
#include "ccm/domain/Enums.hpp"
|
|
#include "ccm/services/CardPreviewService.hpp"
|
|
#include "ccm/services/ImageService.hpp"
|
|
#include "ccm/ui/ImageViewerDialog.hpp"
|
|
#include "ccm/ui/SvgIcons.hpp"
|
|
#include "ccm/ui/Theme.hpp"
|
|
|
|
#include <wx/app.h>
|
|
#include <wx/arrstr.h>
|
|
#include <wx/bitmap.h>
|
|
#include <wx/colour.h>
|
|
#include <wx/event.h>
|
|
#include <wx/filename.h>
|
|
#include <wx/image.h>
|
|
#include <wx/listbox.h>
|
|
#include <wx/log.h>
|
|
#include <wx/mstream.h>
|
|
#include <wx/stdpaths.h>
|
|
#include <wx/panel.h>
|
|
#include <wx/settings.h>
|
|
#include <wx/sizer.h>
|
|
#include <wx/statbmp.h>
|
|
#include <wx/stattext.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace ccm::ui {
|
|
|
|
// Single shared event raised whenever a preview fetch resolves.
|
|
// `event.GetString()` carries the human-readable status (empty on success,
|
|
// non-empty on failure). Defined once in `BaseEvents.cpp`.
|
|
wxDECLARE_EVENT(EVT_PREVIEW_STATUS, wxCommandEvent);
|
|
|
|
template <typename TCard>
|
|
class BaseSelectedCardPanel : public wxPanel {
|
|
public:
|
|
using card_type = TCard;
|
|
|
|
void setCard(std::optional<TCard> card) {
|
|
const std::optional<std::uint32_t> newId =
|
|
card ? std::optional<std::uint32_t>{card->id} : std::nullopt;
|
|
const bool fetchTargetChanged = (newId != lastFetchedId_);
|
|
|
|
card_ = std::move(card);
|
|
|
|
auto applyFlagsRow = [this](bool any) {
|
|
flagsRow_->Layout();
|
|
flagsLabel_->Show(any);
|
|
flagsRow_->Show(any);
|
|
};
|
|
|
|
auto applyNote = [this](const std::string& note) {
|
|
const bool has = !note.empty();
|
|
noteValue_->SetLabelText(wxString::FromUTF8(note.c_str()));
|
|
noteLabel_->Show(has);
|
|
noteValue_->Show(has);
|
|
};
|
|
|
|
if (!card_) {
|
|
for (auto& row : detailRows_) {
|
|
row.value->SetLabelText(row.emptyLabel);
|
|
}
|
|
applyNote("");
|
|
for (auto& fi : flagIcons_) fi.icon->Show(false);
|
|
applyFlagsRow(false);
|
|
if (fetchTargetChanged) {
|
|
state_->currentGen.fetch_add(1);
|
|
lastFetchedId_.reset();
|
|
clearPreview();
|
|
previewStatus_->SetLabelText("");
|
|
emitPreviewStatus("");
|
|
}
|
|
} else {
|
|
const auto& c = *card_;
|
|
// First row is "Name" by convention; we paint it before others so
|
|
// it appears at the top with the literal card name.
|
|
for (auto& row : detailRows_) {
|
|
const std::string value = detailValueFor(c, row.key);
|
|
row.value->SetLabelText(wxString::FromUTF8(value.c_str()));
|
|
}
|
|
applyNote(detailValueFor(c, kNoteKey));
|
|
bool anyFlag = false;
|
|
for (auto& fi : flagIcons_) {
|
|
const bool on = isFlagSet(c, fi.key);
|
|
fi.icon->Show(on);
|
|
if (on) anyFlag = true;
|
|
}
|
|
applyFlagsRow(anyFlag);
|
|
if (fetchTargetChanged) startPreviewFetch(c);
|
|
}
|
|
rebuildImageList();
|
|
Layout();
|
|
}
|
|
|
|
void applyTheme(const ThemePalette& palette) {
|
|
SetBackgroundColour(palette.panelBg);
|
|
SetForegroundColour(palette.text);
|
|
if (previewStatus_ != nullptr) {
|
|
previewStatus_->SetBackgroundColour(palette.panelBg);
|
|
previewStatus_->SetForegroundColour(palette.text);
|
|
}
|
|
for (auto& row : detailRows_) {
|
|
if (row.label != nullptr) {
|
|
row.label->SetBackgroundColour(palette.panelBg);
|
|
row.label->SetForegroundColour(palette.text);
|
|
}
|
|
if (row.value != nullptr) {
|
|
row.value->SetBackgroundColour(palette.panelBg);
|
|
row.value->SetForegroundColour(palette.text);
|
|
}
|
|
}
|
|
flagsRow_->SetBackgroundColour(palette.panelBg);
|
|
flagsRow_->SetForegroundColour(palette.text);
|
|
if (flagsLabel_ != nullptr) {
|
|
flagsLabel_->SetBackgroundColour(palette.panelBg);
|
|
flagsLabel_->SetForegroundColour(palette.text);
|
|
}
|
|
if (noteLabel_ != nullptr) {
|
|
noteLabel_->SetBackgroundColour(palette.panelBg);
|
|
noteLabel_->SetForegroundColour(palette.text);
|
|
}
|
|
if (noteValue_ != nullptr) {
|
|
noteValue_->SetBackgroundColour(palette.panelBg);
|
|
noteValue_->SetForegroundColour(palette.text);
|
|
}
|
|
imageList_->SetBackgroundColour(palette.inputBg);
|
|
imageList_->SetForegroundColour(palette.inputText);
|
|
|
|
const std::string textHex = palette.text.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
|
for (auto& fi : flagIcons_) {
|
|
fi.icon->SetBitmap(svgIconBitmap(fi.svg, kFlagIconSize, textHex.c_str()));
|
|
}
|
|
Layout();
|
|
Refresh();
|
|
}
|
|
|
|
~BaseSelectedCardPanel() override {
|
|
// Detach any in-flight worker: late `CallAfter` lambdas check `alive`
|
|
// before touching `panel` so they become no-ops after destruction.
|
|
if (state_) {
|
|
state_->alive.store(false);
|
|
state_->panel = nullptr;
|
|
}
|
|
}
|
|
|
|
protected:
|
|
// Hook descriptors --------------------------------------------------------
|
|
|
|
// Detail row keys are integers chosen by the subclass; the base just
|
|
// forwards them to `detailValueFor`. Reserve negatives for built-ins.
|
|
using DetailKey = int;
|
|
static constexpr DetailKey kNoteKey = -1;
|
|
|
|
struct DetailRowSpec {
|
|
std::string label;
|
|
DetailKey key;
|
|
std::string emptyLabel; // shown when `card_ == nullopt`
|
|
};
|
|
|
|
struct FlagIconSpec {
|
|
const char* svg;
|
|
const char* tooltip;
|
|
DetailKey key;
|
|
};
|
|
|
|
// Subclass declares the labelled value rows of the detail grid (excluding
|
|
// the trailing "Note" row; that one is always present and conventionally
|
|
// appended right before the image list).
|
|
[[nodiscard]] virtual std::vector<DetailRowSpec> declareDetailRows() const = 0;
|
|
|
|
// Subclass declares the flag-icon strip. Order matters — icons render
|
|
// left-to-right in the same order as this list.
|
|
[[nodiscard]] virtual std::vector<FlagIconSpec> declareFlagIcons() const = 0;
|
|
|
|
// Look up the string value for a detail-row key. The base also calls this
|
|
// with `kNoteKey` to fetch the note for the bottom row.
|
|
[[nodiscard]] virtual std::string detailValueFor(const TCard& card, DetailKey key) const = 0;
|
|
|
|
[[nodiscard]] virtual bool isFlagSet(const TCard& card, DetailKey key) const = 0;
|
|
|
|
// Lookup key for the preview API: (name, setId, setNo). setNo can be
|
|
// empty for games that don't use it (Magic).
|
|
[[nodiscard]] virtual std::tuple<std::string, std::string, std::string>
|
|
previewKey(const TCard& card) const = 0;
|
|
|
|
[[nodiscard]] virtual Game gameId() const noexcept = 0;
|
|
|
|
// Preview / card-back routing. Defaults to `gameId()`; Pokemon overrides
|
|
// so Asia cards use the JapanesePokemon preview source + card back.
|
|
[[nodiscard]] virtual Game previewGameFor(const TCard& /*card*/) const noexcept {
|
|
return gameId();
|
|
}
|
|
|
|
// Construction ------------------------------------------------------------
|
|
|
|
BaseSelectedCardPanel(wxWindow* parent,
|
|
ImageService& imageService,
|
|
CardPreviewService& cardPreview)
|
|
: wxPanel(parent, wxID_ANY),
|
|
imageService_(imageService),
|
|
cardPreview_(cardPreview),
|
|
state_(std::make_shared<PreviewState>()) {
|
|
wxFileName exe(wxStandardPaths::Get().GetExecutablePath());
|
|
exe.SetFullName(wxEmptyString);
|
|
exeDirForBundledAssets_ = exe.GetPathWithSep().ToStdString(wxConvUTF8);
|
|
state_->panel = this;
|
|
}
|
|
|
|
// Subclass calls this once from its constructor after the virtual hooks
|
|
// are reachable.
|
|
void buildLayout() {
|
|
auto* root = new wxBoxSizer(wxVERTICAL);
|
|
|
|
previewBitmap_ = new wxStaticBitmap(this, wxID_ANY, makePreviewPlaceholder());
|
|
previewStatus_ = new wxStaticText(this, wxID_ANY, "");
|
|
root->Add(previewBitmap_, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxBOTTOM, 6);
|
|
root->Add(previewStatus_, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 4);
|
|
|
|
buildInfoGrid(root);
|
|
|
|
SetSizer(root);
|
|
setCard(std::nullopt);
|
|
}
|
|
|
|
private:
|
|
// Shared state for the async preview fetcher.
|
|
struct PreviewState {
|
|
std::atomic<bool> alive{true};
|
|
std::atomic<unsigned> currentGen{0};
|
|
BaseSelectedCardPanel* panel;
|
|
};
|
|
|
|
struct DetailRow {
|
|
wxStaticText* label;
|
|
wxStaticText* value;
|
|
DetailKey key;
|
|
std::string emptyLabel;
|
|
};
|
|
|
|
struct FlagIcon {
|
|
wxStaticBitmap* icon;
|
|
const char* svg;
|
|
DetailKey key;
|
|
};
|
|
|
|
static constexpr int kPreviewWidth = 250;
|
|
static constexpr int kPreviewHeight = 350;
|
|
static constexpr int kImageListWidth = 0;
|
|
static constexpr int kImageListHeight = 80;
|
|
static constexpr int kFlagIconSize = 14;
|
|
|
|
static wxBitmap makePreviewPlaceholder() {
|
|
wxImage img(kPreviewWidth, kPreviewHeight);
|
|
img.SetAlpha();
|
|
if (auto* alpha = img.GetAlpha()) {
|
|
std::fill(alpha, alpha + kPreviewWidth * kPreviewHeight, 0);
|
|
}
|
|
return wxBitmap(img);
|
|
}
|
|
|
|
static std::string fallbackImageUrlForGame(Game game) {
|
|
switch (game) {
|
|
case Game::Magic:
|
|
// Mirrors CCM2's unresolved-preview fallback image.
|
|
return "https://gamepedia.cursecdn.com/mtgsalvation_gamepedia/f/f8/Magic_card_back.jpg";
|
|
case Game::Pokemon:
|
|
// Mirrors CCM2's unresolved-preview fallback image.
|
|
return "https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg";
|
|
case Game::JapanesePokemon:
|
|
// Japanese TCG back (distinct from the Western Cardback.jpg).
|
|
return "https://archives.bulbagarden.net/media/upload/2/2a/TCG_Card_Back_Japanese.jpg";
|
|
case Game::YuGiOh:
|
|
// Yugipedia English TCG backing (thumbnail — smaller than full scan).
|
|
return "https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/250px-Back-EN.png";
|
|
case Game::DigiBattle99:
|
|
// No stable public Digi-Battle back URL; UI uses bundled PNG.
|
|
return {};
|
|
case Game::YuGiOhBandai:
|
|
return "https://ms.yugipedia.com//3/34/Back-BAN-JP-1999.png";
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void buildInfoGrid(wxBoxSizer* root) {
|
|
auto* grid = new wxFlexGridSizer(/*cols=*/2, /*vgap=*/4, /*hgap=*/12);
|
|
grid->AddGrowableCol(1, 1);
|
|
|
|
auto makeBoldLabel = [this](const wxString& text) {
|
|
auto* lbl = new wxStaticText(this, wxID_ANY, text);
|
|
wxFont lf = lbl->GetFont();
|
|
lf.MakeBold();
|
|
lbl->SetFont(lf);
|
|
return lbl;
|
|
};
|
|
|
|
auto specs = declareDetailRows();
|
|
detailRows_.reserve(specs.size());
|
|
for (const auto& spec : specs) {
|
|
auto* lbl = makeBoldLabel(spec.label);
|
|
auto* val = new wxStaticText(this, wxID_ANY, "");
|
|
grid->Add(lbl, 0, wxALIGN_TOP | wxALIGN_LEFT);
|
|
grid->Add(val, 1, wxEXPAND | wxALIGN_LEFT);
|
|
detailRows_.push_back({lbl, val, spec.key, spec.emptyLabel});
|
|
}
|
|
|
|
// Flags row: empty label cell, value cell holds the icon strip.
|
|
flagsLabel_ = new wxStaticText(this, wxID_ANY, "");
|
|
flagsRow_ = new wxPanel(this, wxID_ANY);
|
|
auto* flagsSizer = new wxBoxSizer(wxHORIZONTAL);
|
|
const std::string textHex =
|
|
wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)
|
|
.GetAsString(wxC2S_HTML_SYNTAX)
|
|
.ToStdString();
|
|
auto flagSpecs = declareFlagIcons();
|
|
flagIcons_.reserve(flagSpecs.size());
|
|
for (const auto& fs : flagSpecs) {
|
|
auto* ico = new wxStaticBitmap(flagsRow_, wxID_ANY,
|
|
svgIconBitmap(fs.svg, kFlagIconSize, textHex.c_str()));
|
|
ico->SetToolTip(fs.tooltip);
|
|
flagsSizer->Add(ico, 0, wxRIGHT, 6);
|
|
flagIcons_.push_back({ico, fs.svg, fs.key});
|
|
}
|
|
flagsRow_->SetSizer(flagsSizer);
|
|
grid->Add(flagsLabel_, 0, wxALIGN_TOP | wxALIGN_LEFT);
|
|
grid->Add(flagsRow_, 0, wxEXPAND);
|
|
|
|
// Note row.
|
|
noteLabel_ = makeBoldLabel("Note");
|
|
noteValue_ = new wxStaticText(this, wxID_ANY, "");
|
|
grid->Add(noteLabel_, 0, wxALIGN_TOP | wxALIGN_LEFT);
|
|
grid->Add(noteValue_, 1, wxEXPAND | wxALIGN_LEFT);
|
|
|
|
// Image list row.
|
|
imageList_ = new wxListBox(this, wxID_ANY,
|
|
wxDefaultPosition,
|
|
wxSize(kImageListWidth, kImageListHeight),
|
|
0, nullptr, wxLB_SINGLE);
|
|
imageList_->Bind(wxEVT_LISTBOX_DCLICK, &BaseSelectedCardPanel::onImageActivated, this);
|
|
grid->Add(makeBoldLabel("Images"), 0, wxALIGN_TOP | wxALIGN_LEFT);
|
|
grid->Add(imageList_, 1, wxEXPAND);
|
|
|
|
root->Add(grid, 1, wxEXPAND | wxALL, 8);
|
|
}
|
|
|
|
void clearPreview() {
|
|
previewBitmap_->SetBitmap(makePreviewPlaceholder());
|
|
}
|
|
|
|
void startPreviewFetch(const TCard& card) {
|
|
clearPreview();
|
|
previewStatus_->SetLabelText("Loading preview...");
|
|
emitPreviewStatus("");
|
|
lastFetchedId_ = card.id;
|
|
Layout();
|
|
|
|
const unsigned gen = state_->currentGen.fetch_add(1) + 1;
|
|
|
|
auto state = state_;
|
|
CardPreviewService* svcPtr = &cardPreview_;
|
|
auto [name, setId, setNo] = previewKey(card);
|
|
const Game game = previewGameFor(card);
|
|
const std::string exeDirCopy = exeDirForBundledAssets_;
|
|
|
|
std::thread([state, gen, svcPtr, name = std::move(name),
|
|
setId = std::move(setId), setNo = std::move(setNo), game,
|
|
exeDirCopy]() {
|
|
auto bytes = svcPtr->fetchPreviewBytes(game, name, setId, setNo);
|
|
bool ok = bytes.isOk();
|
|
bool usedFallback = false;
|
|
std::string payload = ok ? std::move(bytes).value() : std::string{};
|
|
std::string err = ok ? std::string{} : bytes.error();
|
|
|
|
auto applyFallbackPayload = [&](std::string p) {
|
|
if (p.empty()) return;
|
|
payload = std::move(p);
|
|
ok = true;
|
|
usedFallback = true;
|
|
err.clear();
|
|
};
|
|
|
|
if (!ok || payload.empty()) {
|
|
if (game == Game::YuGiOh) {
|
|
if (auto fb =
|
|
svcPtr->fetchImageBytesByUrl(
|
|
"https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/"
|
|
"250px-Back-EN.png");
|
|
fb.isOk()) {
|
|
applyFallbackPayload(std::move(fb).value());
|
|
}
|
|
if (!ok || payload.empty()) {
|
|
if (auto fb = svcPtr->fetchImageBytesByUrl(
|
|
"https://ms.yugipedia.com/e/e5/Back-EN.png");
|
|
fb.isOk()) {
|
|
applyFallbackPayload(std::move(fb).value());
|
|
}
|
|
}
|
|
if (!ok || payload.empty()) {
|
|
namespace fs = std::filesystem;
|
|
const fs::path asset =
|
|
fs::path(exeDirCopy) / "assets" / "ygo_card_back.png";
|
|
std::ifstream in(asset, std::ios::binary);
|
|
if (in) {
|
|
std::ostringstream ss;
|
|
ss << in.rdbuf();
|
|
applyFallbackPayload(ss.str());
|
|
}
|
|
}
|
|
} else if (game == Game::DigiBattle99) {
|
|
namespace fs = std::filesystem;
|
|
const fs::path asset =
|
|
fs::path(exeDirCopy) / "assets" / "digibattle99_card_back.png";
|
|
std::ifstream in(asset, std::ios::binary);
|
|
if (in) {
|
|
std::ostringstream ss;
|
|
ss << in.rdbuf();
|
|
applyFallbackPayload(ss.str());
|
|
}
|
|
} else {
|
|
const std::string fallbackUrl = fallbackImageUrlForGame(game);
|
|
if (!fallbackUrl.empty()) {
|
|
auto fallbackBytes = svcPtr->fetchImageBytesByUrl(fallbackUrl);
|
|
if (fallbackBytes.isOk()) {
|
|
applyFallbackPayload(std::move(fallbackBytes).value());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wxTheApp->CallAfter(
|
|
[state, gen, ok, usedFallback,
|
|
payload = std::move(payload), err = std::move(err)]() mutable {
|
|
if (!state->alive.load()) return;
|
|
if (state->currentGen.load() != gen) return;
|
|
if (state->panel == nullptr) return;
|
|
state->panel->onPreviewBytes(gen, ok, usedFallback,
|
|
std::move(payload), std::move(err));
|
|
});
|
|
}).detach();
|
|
}
|
|
|
|
void onPreviewBytes(unsigned gen, bool ok, bool usedFallback,
|
|
std::string bytes, std::string err) {
|
|
if (gen != state_->currentGen.load()) return;
|
|
if (!ok || bytes.empty()) {
|
|
previewStatus_->SetLabelText("(no preview available)");
|
|
clearPreview();
|
|
Layout();
|
|
wxString detail = err.empty()
|
|
? wxString("no preview returned")
|
|
: wxString::FromUTF8(err);
|
|
emitPreviewStatus("Preview unavailable: " + detail);
|
|
return;
|
|
}
|
|
|
|
wxMemoryInputStream stream(bytes.data(), bytes.size());
|
|
wxImage img;
|
|
// libpng emits iCCP warnings for some upstream PNGs (e.g. Yugipedia
|
|
// scans with embedded sRGB chunks wx considers invalid). wx forwards
|
|
// those as wxLogWarning -> modal dialog. Suppress logging for decode
|
|
// only; the pixels load correctly either way.
|
|
bool decoded = false;
|
|
{
|
|
wxLogNull suppressPngWarnings;
|
|
decoded = img.LoadFile(stream, wxBITMAP_TYPE_ANY);
|
|
}
|
|
if (!decoded) {
|
|
previewStatus_->SetLabelText("(preview decode failed)");
|
|
clearPreview();
|
|
Layout();
|
|
emitPreviewStatus("Preview unavailable: image decode failed");
|
|
return;
|
|
}
|
|
if (img.GetWidth() != kPreviewWidth || img.GetHeight() != kPreviewHeight) {
|
|
img.Rescale(kPreviewWidth, kPreviewHeight, wxIMAGE_QUALITY_HIGH);
|
|
}
|
|
previewBitmap_->SetBitmap(wxBitmap(img));
|
|
if (usedFallback) {
|
|
previewStatus_->SetLabelText("(image preview unavailable)");
|
|
emitPreviewStatus("Preview unavailable: showing fallback card-back image.");
|
|
} else {
|
|
previewStatus_->SetLabelText("");
|
|
emitPreviewStatus("");
|
|
}
|
|
Layout();
|
|
}
|
|
|
|
void emitPreviewStatus(const wxString& message) {
|
|
wxCommandEvent ev(EVT_PREVIEW_STATUS, GetId());
|
|
ev.SetEventObject(this);
|
|
ev.SetString(message);
|
|
if (auto* parent = GetParent()) {
|
|
parent->GetEventHandler()->ProcessEvent(ev);
|
|
} else {
|
|
ProcessWindowEvent(ev);
|
|
}
|
|
}
|
|
|
|
void rebuildImageList() {
|
|
imageList_->Clear();
|
|
if (!card_) return;
|
|
for (std::size_t i = 0; i < card_->images.size(); ++i) {
|
|
imageList_->Append(wxString::Format("Image %zu", i + 1));
|
|
}
|
|
}
|
|
|
|
void onImageActivated(wxCommandEvent& event) {
|
|
if (!card_) return;
|
|
const int sel = event.GetSelection();
|
|
if (sel < 0 || static_cast<std::size_t>(sel) >= card_->images.size()) return;
|
|
|
|
std::vector<std::filesystem::path> paths;
|
|
paths.reserve(card_->images.size());
|
|
for (const auto& name : card_->images) {
|
|
paths.push_back(imageService_.resolveImagePath(gameId(), name));
|
|
}
|
|
ImageViewerDialog dlg(this, std::move(paths), static_cast<std::size_t>(sel));
|
|
const Theme theme = inferThemeFromWindow(this);
|
|
applyThemeToWindowTree(&dlg, paletteForTheme(theme), theme);
|
|
dlg.ShowModal();
|
|
}
|
|
|
|
ImageService& imageService_;
|
|
CardPreviewService& cardPreview_;
|
|
std::string exeDirForBundledAssets_;
|
|
std::optional<TCard> card_;
|
|
|
|
wxStaticBitmap* previewBitmap_{nullptr};
|
|
wxStaticText* previewStatus_{nullptr};
|
|
|
|
std::vector<DetailRow> detailRows_;
|
|
|
|
wxStaticText* flagsLabel_{nullptr};
|
|
wxPanel* flagsRow_{nullptr};
|
|
std::vector<FlagIcon> flagIcons_;
|
|
|
|
wxStaticText* noteLabel_{nullptr};
|
|
wxStaticText* noteValue_{nullptr};
|
|
|
|
wxListBox* imageList_{nullptr};
|
|
|
|
std::shared_ptr<PreviewState> state_;
|
|
std::optional<std::uint32_t> lastFetchedId_;
|
|
};
|
|
|
|
} // namespace ccm::ui
|