Minor: Add Asian Pokemon Card Support (#18)

This commit is contained in:
Sebastian Dine
2026-07-22 11:13:42 +02:00
committed by GitHub
parent e5c830e945
commit c9e6bc2b6b
87 changed files with 78068 additions and 162 deletions
+5 -4
View File
@@ -41,10 +41,11 @@ constexpr const char kFilterInputHint[] = "Filter";
std::string dirNameForGame(Game g) {
switch (g) {
case Game::Magic: return "magic";
case Game::Pokemon: return "pokemon";
case Game::YuGiOh: return "yugioh";
case Game::DigiBattle99: return "digibattle99";
case Game::Magic: return "magic";
case Game::Pokemon: return "pokemon";
case Game::YuGiOh: return "yugioh";
case Game::DigiBattle99: return "digibattle99";
case Game::JapanesePokemon: return "pokemon";
}
return "magic";
}
+345 -26
View File
@@ -1,12 +1,34 @@
#include "ccm/ui/PokemonCardEditDialog.hpp"
#include "ccm/domain/Enums.hpp"
#include "ccm/games/pokemonjp/JapanesePokemonCardPreviewSource.hpp"
#include "ccm/ui/Theme.hpp"
#include "ccm/ui/VariantImagePreviewDialog.hpp"
#include <wx/app.h>
#include <wx/panel.h>
#include <thread>
#include <unordered_set>
namespace ccm::ui {
namespace {
constexpr const char* kUnnumberedPromoSetId = "UnnumberedPromo";
constexpr const char* kJapanesePokemonCardBackUrl =
"https://archives.bulbagarden.net/media/upload/2/2a/TCG_Card_Back_Japanese.jpg";
const std::vector<Set> kEmptySets;
bool languageAllowedForRegion(Language lang, PokemonRegion region) {
for (const auto l : languagesForPokemonRegion(region)) {
if (l == lang) return true;
}
return false;
}
} // namespace
PokemonCardEditDialog::PokemonCardEditDialog(wxWindow* parent,
ImageService& imageService,
@@ -14,15 +36,22 @@ PokemonCardEditDialog::PokemonCardEditDialog(wxWindow* parent,
CardPreviewService& cardPreview,
EditMode mode,
PokemonCard initial,
const std::vector<Set>* preloadedSets)
const std::vector<Set>* westSets,
const std::vector<Set>* asiaSets)
: BaseCardEditDialog<PokemonCard>(
parent,
mode == EditMode::Create ? "Add Pokemon Card" : "Edit Pokemon Card",
imageService, setService, mode, std::move(initial), Game::Pokemon, preloadedSets),
imageService, setService, mode, PokemonCard{}, Game::Pokemon, nullptr),
dialogMode_(mode),
cardPreview_(cardPreview),
westSets_(westSets),
asiaSets_(asiaSets),
variantFetchState_(std::make_shared<VariantFetchState>()) {
const PokemonRegion region = initial.region;
mutableCard() = std::move(initial);
setPreloadedSetsPointer(region == PokemonRegion::Asia ? asiaSets_ : westSets_);
buildAndPopulate();
refreshSetNoRowMode();
if (dialogMode_ == EditMode::Edit) {
scheduleDeferredVariantPrefetch();
}
@@ -32,12 +61,40 @@ PokemonCardEditDialog::~PokemonCardEditDialog() {
if (variantFetchState_) {
variantFetchState_->alive.store(false);
}
closeUnnumberedPreview();
}
PokemonRegion PokemonCardEditDialog::currentRegion() const noexcept {
return constCard().region;
}
Game PokemonCardEditDialog::backendGame() const noexcept {
return pokemonBackendGame(currentRegion());
}
std::span<const Language> PokemonCardEditDialog::languagesForChoice() const {
return languagesForPokemonRegion(currentRegion());
}
void PokemonCardEditDialog::onCardLookupContextChanged() {
clearCachedPrintVariants();
}
void PokemonCardEditDialog::appendPreSetRows(wxFlexGridSizer* grid) {
auto* regionPanel = new wxPanel(this, wxID_ANY);
auto* row = new wxBoxSizer(wxHORIZONTAL);
regionSwitch_ = new SwitchCtrl(regionPanel, wxID_ANY,
constCard().region == PokemonRegion::Asia);
row->Add(new wxStaticText(regionPanel, wxID_ANY, wxString::FromUTF8("West")),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
row->Add(regionSwitch_, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
row->Add(new wxStaticText(regionPanel, wxID_ANY, wxString::FromUTF8("Asia")),
0, wxALIGN_CENTER_VERTICAL);
regionPanel->SetSizer(row);
regionSwitch_->Bind(EVT_CCM_SWITCH, &PokemonCardEditDialog::onRegionSwitch, this);
appendRow(grid, "Region", regionPanel);
}
void PokemonCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
holoCheck_ = new wxCheckBox(this, wxID_ANY, "Holo");
firstEditionCheck_ = new wxCheckBox(this, wxID_ANY, "1. Edition");
@@ -50,6 +107,7 @@ void PokemonCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
}
void PokemonCardEditDialog::appendExtraRows(wxFlexGridSizer* grid) {
setNoLabel_ = new wxStaticText(this, wxID_ANY, "Set #");
auto* setNoPanel = new wxPanel(this, wxID_ANY);
setNoCtrl_ = new wxTextCtrl(setNoPanel, wxID_ANY);
autoSetNoBtn_ = new wxButton(setNoPanel, wxID_ANY, "Auto detect");
@@ -63,20 +121,63 @@ void PokemonCardEditDialog::appendExtraRows(wxFlexGridSizer* grid) {
setNoRow->Add(nextSetNoBtn_, 0, wxALIGN_CENTER_VERTICAL);
setNoPanel->SetSizer(setNoRow);
appendRow(grid, "Set #", setNoPanel);
grid->Add(setNoLabel_, 0, wxALIGN_CENTER_VERTICAL);
grid->Add(setNoPanel, 1, wxEXPAND);
if (auto* setCombo = setComboControl()) {
setCombo->Bind(wxEVT_COMBOBOX, &PokemonCardEditDialog::onSetSelectionChanged, this);
}
}
std::string PokemonCardEditDialog::normalizedStoredSetNo(std::string_view setNo) {
std::string out(setNo);
const auto slash = out.find('/');
if (slash != std::string::npos) {
out.resize(slash);
void PokemonCardEditDialog::onRegionSwitch(wxCommandEvent&) {
const PokemonRegion next =
(regionSwitch_ && regionSwitch_->GetValue()) ? PokemonRegion::Asia
: PokemonRegion::West;
applyRegion(next, true);
}
void PokemonCardEditDialog::applyRegion(PokemonRegion region, bool clearSetIfMissing) {
mutableCard().region = region;
if (regionSwitch_ && regionSwitch_->GetValue() != (region == PokemonRegion::Asia)) {
regionSwitch_->SetValue(region == PokemonRegion::Asia, false);
}
return out;
if (!languageAllowedForRegion(mutableCard().language, region)) {
mutableCard().language = defaultLanguageForPokemonRegion(region);
}
const std::vector<Set>* sets =
region == PokemonRegion::Asia
? (asiaSets_ != nullptr ? asiaSets_ : &kEmptySets)
: (westSets_ != nullptr ? westSets_ : &kEmptySets);
setPreloadedSetsPointer(sets);
const std::string prevSetId = mutableCard().set.id;
bool setStillValid = false;
for (const auto& s : availableSets()) {
if (s.id == prevSetId) {
setStillValid = true;
mutableCard().set = s;
break;
}
}
if (clearSetIfMissing && !setStillValid) {
mutableCard().set = Set{};
mutableCard().setNo.clear();
selectedSetNo_.clear();
if (setNoCtrl_) setNoCtrl_->ChangeValue(wxEmptyString);
}
refreshSetAndLanguageChoices();
clearCachedPrintVariants();
refreshSetNoRowMode();
scheduleDeferredVariantPrefetch();
Layout();
if (GetSizer()) Fit();
}
std::string PokemonCardEditDialog::normalizedStoredSetNo(std::string_view setNo) {
return JapanesePokemonCardPreviewSource::normalizeLocalId(setNo);
}
std::string PokemonCardEditDialog::storedSetNoFromControls(const wxTextCtrl* ctrl) {
@@ -84,20 +185,78 @@ std::string PokemonCardEditDialog::storedSetNoFromControls(const wxTextCtrl* ctr
return normalizedStoredSetNo(ctrl->GetValue().ToStdString(wxConvUTF8));
}
bool PokemonCardEditDialog::isUnnumberedPromoSelected() const {
if (currentRegion() != PokemonRegion::Asia) return false;
if (const auto* set = selectedSetFromControls()) {
return set->id == kUnnumberedPromoSetId;
}
return constCard().set.id == kUnnumberedPromoSetId;
}
std::string PokemonCardEditDialog::currentRingSetNo() const {
if (!uniqueSetNos_.empty() && setNoRingPos_ < uniqueSetNos_.size()) {
return uniqueSetNos_[setNoRingPos_];
}
return selectedSetNo_;
}
void PokemonCardEditDialog::applySelectedSetNo(std::string setNo) {
selectedSetNo_ = normalizedStoredSetNo(setNo);
if (setNoCtrl_ && setNoCtrl_->IsShown()) {
setNoCtrl_->ChangeValue(wxString::FromUTF8(selectedSetNo_.c_str()));
}
}
void PokemonCardEditDialog::refreshSetNoRowMode() {
const bool unnumbered = isUnnumberedPromoSelected();
if (setNoLabel_) {
setNoLabel_->SetLabelText(unnumbered ? wxString::FromUTF8("Print")
: wxString::FromUTF8("Set #"));
}
if (setNoCtrl_) {
setNoCtrl_->Show(!unnumbered);
if (!unnumbered && !selectedSetNo_.empty()) {
setNoCtrl_->ChangeValue(wxString::FromUTF8(selectedSetNo_.c_str()));
}
if (auto* parent = setNoCtrl_->GetParent()) {
parent->Layout();
}
}
if (!unnumbered) {
closeUnnumberedPreview();
}
Layout();
if (GetSizer()) Fit();
}
void PokemonCardEditDialog::readExtraFromCard() {
clearCachedPrintVariants();
selectedSetNo_ = normalizedStoredSetNo(constCard().setNo);
if (setNoCtrl_) {
setNoCtrl_->ChangeValue(
wxString::FromUTF8(normalizedStoredSetNo(constCard().setNo).c_str()));
setNoCtrl_->ChangeValue(wxString::FromUTF8(selectedSetNo_.c_str()));
}
if (holoCheck_) holoCheck_->SetValue(constCard().holo);
if (firstEditionCheck_) firstEditionCheck_->SetValue(constCard().firstEdition);
if (signedCheck_) signedCheck_->SetValue(constCard().signed_);
if (alteredCheck_) alteredCheck_->SetValue(constCard().altered);
if (regionSwitch_) {
regionSwitch_->SetValue(constCard().region == PokemonRegion::Asia, false);
}
refreshSetNoRowMode();
}
void PokemonCardEditDialog::writeExtraToCard() {
if (setNoCtrl_) mutableCard().setNo = storedSetNoFromControls(setNoCtrl_);
mutableCard().region =
(regionSwitch_ && regionSwitch_->GetValue()) ? PokemonRegion::Asia
: PokemonRegion::West;
if (isUnnumberedPromoSelected()) {
mutableCard().setNo = normalizedStoredSetNo(selectedSetNo_);
} else if (setNoCtrl_) {
selectedSetNo_ = storedSetNoFromControls(setNoCtrl_);
mutableCard().setNo = selectedSetNo_;
} else {
mutableCard().setNo = normalizedStoredSetNo(selectedSetNo_);
}
if (holoCheck_) mutableCard().holo = holoCheck_->IsChecked();
if (firstEditionCheck_) mutableCard().firstEdition = firstEditionCheck_->IsChecked();
if (signedCheck_) mutableCard().signed_ = signedCheck_->IsChecked();
@@ -106,9 +265,11 @@ void PokemonCardEditDialog::writeExtraToCard() {
void PokemonCardEditDialog::clearCachedPrintVariants() {
++variantFetchEpoch_;
++previewFetchEpoch_;
cachedVariants_.clear();
uniqueSetNos_.clear();
setNoRingPos_ = 0;
closeUnnumberedPreview();
refreshVariantNextControls();
}
@@ -142,9 +303,10 @@ void PokemonCardEditDialog::requestVariantsAsync(unsigned capturedEpoch,
auto state = variantFetchState_;
CardPreviewService* svc = &cardPreview_;
PokemonCardEditDialog* self = this;
const Game game = backendGame();
std::thread([state, svc, self, capturedEpoch, name = std::move(name),
setId = std::move(setId), fillSetNoOnSuccess, showFailureDialog]() {
auto detected = svc->detectPrintVariants(Game::Pokemon, name, setId);
setId = std::move(setId), fillSetNoOnSuccess, showFailureDialog, game]() {
auto detected = svc->detectPrintVariants(game, name, setId);
wxTheApp->CallAfter([state, self, capturedEpoch, detected = std::move(detected),
fillSetNoOnSuccess, showFailureDialog]() mutable {
if (!state->alive.load()) return;
@@ -173,14 +335,20 @@ void PokemonCardEditDialog::applyDetectedVariants(unsigned capturedEpoch,
}
cachedVariants_ = std::move(detected).value();
if (fillSetNoOnSuccess && setNoCtrl_ && !cachedVariants_.empty()) {
setNoCtrl_->ChangeValue(
wxString::FromUTF8(cachedVariants_.front().setNo.c_str()));
if (fillSetNoOnSuccess && !cachedVariants_.empty()) {
applySelectedSetNo(cachedVariants_.front().setNo);
}
rebuildVariantRingFromCache();
syncRingPositionToControls();
refreshVariantNextControls();
if (isUnnumberedPromoSelected() && !uniqueSetNos_.empty()) {
ensureUnnumberedPreviewOpen();
refreshUnnumberedPreview();
if (unnumberedPreview_ != nullptr) {
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
}
}
}
void PokemonCardEditDialog::rebuildVariantRingFromCache() {
@@ -197,8 +365,12 @@ void PokemonCardEditDialog::rebuildVariantRingFromCache() {
}
void PokemonCardEditDialog::syncRingPositionToControls() {
if (!setNoCtrl_) return;
const std::string current = storedSetNoFromControls(setNoCtrl_);
const std::string current = isUnnumberedPromoSelected()
? normalizedStoredSetNo(selectedSetNo_)
: storedSetNoFromControls(setNoCtrl_);
if (!isUnnumberedPromoSelected() && setNoCtrl_) {
selectedSetNo_ = current;
}
setNoRingPos_ = 0;
for (std::size_t i = 0; i < uniqueSetNos_.size(); ++i) {
if (uniqueSetNos_[i] == current) {
@@ -206,26 +378,66 @@ void PokemonCardEditDialog::syncRingPositionToControls() {
break;
}
}
if (!uniqueSetNos_.empty() && selectedSetNo_.empty()) {
applySelectedSetNo(uniqueSetNos_[setNoRingPos_]);
}
}
void PokemonCardEditDialog::refreshVariantNextControls() {
if (!nextSetNoBtn_) return;
nextSetNoBtn_->Show(uniqueSetNos_.size() > 1);
const bool showNext = uniqueSetNos_.size() > 1;
nextSetNoBtn_->Show(showNext);
if (showNext) {
if (isUnnumberedPromoSelected()) {
const std::size_t i = setNoRingPos_ + 1;
const std::size_t n = uniqueSetNos_.size();
nextSetNoBtn_->SetLabel(wxString::Format("Next (%zu/%zu)", i, n));
} else {
const std::string setNo = currentRingSetNo();
if (setNo.empty()) {
nextSetNoBtn_->SetLabel("Next");
} else {
nextSetNoBtn_->SetLabel(
wxString::Format("Next (%s)", wxString::FromUTF8(setNo.c_str())));
}
}
} else {
nextSetNoBtn_->SetLabel("Next");
}
if (auto* parent = nextSetNoBtn_->GetParent()) {
parent->Layout();
}
Layout();
if (GetSizer()) Fit();
if (unnumberedPreview_ != nullptr) {
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
}
}
void PokemonCardEditDialog::onAutoDetectSetNo(wxCommandEvent&) {
autoDetectFromApi();
}
void PokemonCardEditDialog::onNextSetNo(wxCommandEvent&) {
if (uniqueSetNos_.size() <= 1) return;
setNoRingPos_ = (setNoRingPos_ + 1) % uniqueSetNos_.size();
if (setNoCtrl_) {
setNoCtrl_->ChangeValue(wxString::FromUTF8(uniqueSetNos_[setNoRingPos_].c_str()));
}
void PokemonCardEditDialog::stepVariantRing(int delta) {
if (uniqueSetNos_.size() <= 1 || delta == 0) return;
const auto n = static_cast<int>(uniqueSetNos_.size());
auto pos = static_cast<int>(setNoRingPos_) + delta;
pos %= n;
if (pos < 0) pos += n;
setNoRingPos_ = static_cast<std::size_t>(pos);
applySelectedSetNo(uniqueSetNos_[setNoRingPos_]);
refreshVariantNextControls();
if (isUnnumberedPromoSelected()) {
ensureUnnumberedPreviewOpen();
refreshUnnumberedPreview();
if (unnumberedPreview_ != nullptr) {
unnumberedPreview_->setNavigationEnabled(true);
}
}
}
void PokemonCardEditDialog::onNextSetNo(wxCommandEvent&) {
stepVariantRing(1);
}
void PokemonCardEditDialog::autoDetectFromApi() {
@@ -248,8 +460,115 @@ void PokemonCardEditDialog::autoDetectFromApi() {
void PokemonCardEditDialog::onSetSelectionChanged(wxCommandEvent& ev) {
clearCachedPrintVariants();
refreshSetNoRowMode();
scheduleDeferredVariantPrefetch();
ev.Skip();
}
void PokemonCardEditDialog::closeUnnumberedPreview() {
++previewFetchEpoch_;
if (unnumberedPreview_ != nullptr) {
unnumberedPreview_->Destroy();
unnumberedPreview_ = nullptr;
}
}
void PokemonCardEditDialog::ensureUnnumberedPreviewOpen() {
if (!isUnnumberedPromoSelected()) {
closeUnnumberedPreview();
return;
}
if (unnumberedPreview_ != nullptr) {
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
unnumberedPreview_->repositionBesideParent();
return;
}
unnumberedPreview_ = new VariantImagePreviewDialog(this);
const Theme theme = inferThemeFromWindow(this);
applyThemeToWindowTree(unnumberedPreview_, paletteForTheme(theme), theme);
unnumberedPreview_->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& ev) {
unnumberedPreview_ = nullptr;
ev.Skip();
});
unnumberedPreview_->Bind(EVT_VARIANT_PREVIEW_PREV, [this](wxCommandEvent&) {
stepVariantRing(-1);
});
unnumberedPreview_->Bind(EVT_VARIANT_PREVIEW_NEXT, [this](wxCommandEvent&) {
stepVariantRing(1);
});
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
unnumberedPreview_->Show(true);
unnumberedPreview_->repositionBesideParent();
}
void PokemonCardEditDialog::refreshUnnumberedPreview() {
if (!isUnnumberedPromoSelected() || uniqueSetNos_.empty()) {
closeUnnumberedPreview();
return;
}
ensureUnnumberedPreviewOpen();
if (unnumberedPreview_ == nullptr) return;
syncCardFromControls();
const auto& card = constCard();
const std::string setNo = currentRingSetNo();
if (card.name.empty() || setNo.empty()) {
unnumberedPreview_->clearImage();
unnumberedPreview_->setCaption(wxString::FromUTF8("Enter a card name"));
return;
}
const std::size_t i = setNoRingPos_ + 1;
const std::size_t n = uniqueSetNos_.size();
unnumberedPreview_->setCaption(
wxString::Format("%s (%zu/%zu)",
wxString::FromUTF8(card.name.c_str()), i, n));
const unsigned epoch = ++previewFetchEpoch_;
requestUnnumberedPreviewAsync(epoch, card.name, kUnnumberedPromoSetId, setNo, setNoRingPos_,
uniqueSetNos_.size());
}
void PokemonCardEditDialog::requestUnnumberedPreviewAsync(unsigned capturedEpoch,
std::string name,
std::string setId,
std::string setNo,
std::size_t ringIndex,
std::size_t ringCount) {
auto state = variantFetchState_;
CardPreviewService* svc = &cardPreview_;
PokemonCardEditDialog* self = this;
std::thread([state, svc, self, capturedEpoch, name = std::move(name),
setId = std::move(setId), setNo = std::move(setNo), ringIndex,
ringCount]() {
auto bytes = svc->fetchPreviewBytes(Game::JapanesePokemon, name, setId, setNo);
std::string payload;
bool usedFallback = false;
if (bytes) {
payload = std::move(bytes).value();
} else {
auto fallback = svc->fetchImageBytesByUrl(kJapanesePokemonCardBackUrl);
if (fallback) {
payload = std::move(fallback).value();
usedFallback = true;
}
}
wxTheApp->CallAfter([state, self, capturedEpoch, payload = std::move(payload),
name, ringIndex, ringCount, usedFallback]() mutable {
if (!state->alive.load()) return;
if (capturedEpoch != self->previewFetchEpoch_) return;
if (self->unnumberedPreview_ == nullptr) return;
self->unnumberedPreview_->setImageBytes(payload);
if (usedFallback) {
self->unnumberedPreview_->setCaption(
wxString::Format("%s (%zu/%zu) — preview unavailable",
wxString::FromUTF8(name.c_str()),
ringIndex + 1, ringCount));
}
});
}).detach();
}
} // namespace ccm::ui
+70 -25
View File
@@ -31,18 +31,22 @@ void PokemonGameView::ensureSetsLoaded() {
if (attemptedInitialSetLoad_) return;
attemptedInitialSetLoad_ = true;
auto cached = sets_.getSets(Game::Pokemon);
if (cached) {
setsCache_ = std::move(cached).value();
if (!setsCache_.empty()) return;
} else {
setsCache_.clear();
}
auto loadOrRefresh = [this](Game game, std::vector<Set>& cache) {
auto cached = sets_.getSets(game);
if (cached) {
cache = std::move(cached).value();
if (!cache.empty()) return;
} else {
cache.clear();
}
auto refreshed = sets_.updateSets(game);
if (refreshed) {
cache = std::move(refreshed).value();
}
};
auto refreshed = sets_.updateSets(Game::Pokemon);
if (refreshed) {
setsCache_ = std::move(refreshed).value();
}
loadOrRefresh(Game::Pokemon, setsCacheWest_);
loadOrRefresh(Game::JapanesePokemon, setsCacheAsia_);
}
wxPanel* PokemonGameView::listPanel(wxWindow* parent) {
@@ -81,13 +85,20 @@ void PokemonGameView::refreshCollection() {
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
}
const std::vector<Set>& PokemonGameView::setsForDialog() {
const std::vector<Set>& PokemonGameView::setsForDialog(PokemonRegion region) {
ensureSetsLoaded();
if (!setsCache_.empty()) return setsCache_;
if (region == PokemonRegion::Asia) {
if (!setsCacheAsia_.empty()) return setsCacheAsia_;
auto loaded = sets_.getSets(Game::JapanesePokemon);
if (loaded) setsCacheAsia_ = std::move(loaded).value();
else setsCacheAsia_.clear();
return setsCacheAsia_;
}
if (!setsCacheWest_.empty()) return setsCacheWest_;
auto loaded = sets_.getSets(Game::Pokemon);
if (loaded) setsCache_ = std::move(loaded).value();
else setsCache_.clear();
return setsCache_;
if (loaded) setsCacheWest_ = std::move(loaded).value();
else setsCacheWest_.clear();
return setsCacheWest_;
}
void PokemonGameView::onAddCard(wxWindow* parentWindow) {
@@ -98,11 +109,13 @@ void PokemonGameView::onAddCard(wxWindow* parentWindow) {
}
PokemonCard fresh;
fresh.amount = 1;
fresh.region = PokemonRegion::West;
fresh.language = Language::English;
fresh.condition = Condition::NearMint;
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Create, fresh,
&setsForDialog());
&setsForDialog(PokemonRegion::West),
&setsForDialog(PokemonRegion::Asia));
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
@@ -147,7 +160,8 @@ void PokemonGameView::onEditCard(wxWindow* parentWindow) {
return;
}
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Edit, *sel,
&setsForDialog());
&setsForDialog(PokemonRegion::West),
&setsForDialog(PokemonRegion::Asia));
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
@@ -181,15 +195,46 @@ void PokemonGameView::onDeleteCard(wxWindow* parentWindow) {
}
std::string PokemonGameView::onUpdateSets(wxWindow* parentWindow) {
auto out = sets_.updateSets(Game::Pokemon);
if (!out) {
showThemedMessageDialog(parentWindow, "Failed to update sets: " + out.error(),
"Error", wxOK | wxICON_ERROR);
auto westOut = sets_.updateSets(Game::Pokemon);
auto asiaOut = sets_.updateSets(Game::JapanesePokemon);
if (westOut) {
setsCacheWest_ = westOut.value();
}
if (asiaOut) {
setsCacheAsia_ = asiaOut.value();
}
if (!westOut && !asiaOut) {
showThemedMessageDialog(
parentWindow,
"Failed to update West sets: " + westOut.error() +
"\nFailed to update Asia sets: " + asiaOut.error(),
"Error", wxOK | wxICON_ERROR);
return "Update failed";
}
setsCache_ = out.value();
showThemedMessageDialog(parentWindow, "Updated " + std::to_string(out.value().size()) + " Pokemon sets.",
"Sets updated", wxOK | wxICON_INFORMATION);
if (!westOut) {
showThemedMessageDialog(
parentWindow,
"Updated " + std::to_string(asiaOut.value().size()) +
" Asia Pokemon sets, but West failed: " + westOut.error(),
"Sets partially updated", wxOK | wxICON_WARNING);
return "Pokemon sets partially updated.";
}
if (!asiaOut) {
showThemedMessageDialog(
parentWindow,
"Updated " + std::to_string(westOut.value().size()) +
" West Pokemon sets, but Asia failed: " + asiaOut.error(),
"Sets partially updated", wxOK | wxICON_WARNING);
return "Pokemon sets partially updated.";
}
showThemedMessageDialog(
parentWindow,
"Updated " + std::to_string(westOut.value().size()) + " West and " +
std::to_string(asiaOut.value().size()) + " Asia Pokemon sets.",
"Sets updated", wxOK | wxICON_INFORMATION);
return "Pokemon sets updated.";
}
+3
View File
@@ -11,6 +11,7 @@ enum PokemonDetailKey : int {
kName = 0,
kSet,
kSetNo,
kRegion,
kLanguage,
kCondition,
kAmount,
@@ -34,6 +35,7 @@ PokemonSelectedCardPanel::declareDetailRows() const {
{"Name", kName, "(no card selected)"},
{"Set", kSet, ""},
{"Set #", kSetNo, ""},
{"Region", kRegion, ""},
{"Language", kLanguage, ""},
{"Condition", kCondition, ""},
{"Amount", kAmount, ""},
@@ -56,6 +58,7 @@ std::string PokemonSelectedCardPanel::detailValueFor(const PokemonCard& card,
case kName: return card.name;
case kSet: return card.set.name;
case kSetNo: return card.setNo;
case kRegion: return std::string(to_string(card.region));
case kLanguage: return std::string(to_string(card.language));
case kCondition: return std::string(to_string(card.condition));
case kAmount: return std::to_string(card.amount);
+5 -4
View File
@@ -14,10 +14,11 @@ namespace {
wxString displayLabelForGame(Game g) {
switch (g) {
case Game::Magic: return "Magic";
case Game::Pokemon: return "Pokemon";
case Game::YuGiOh: return "Yu-Gi-Oh!";
case Game::DigiBattle99: return "Digimon (Digi-Battle)";
case Game::Magic: return "Magic";
case Game::Pokemon: return "Pokemon";
case Game::YuGiOh: return "Yu-Gi-Oh!";
case Game::DigiBattle99: return "Digimon (Digi-Battle)";
case Game::JapanesePokemon: return "Pokemon"; // internal; not in allGames()
}
return wxString::FromUTF8(to_string(g).data());
}
+176
View File
@@ -0,0 +1,176 @@
#include "ccm/ui/VariantImagePreviewDialog.hpp"
#include <wx/bitmap.h>
#include <wx/dcclient.h>
#include <wx/display.h>
#include <wx/log.h>
#include <wx/mstream.h>
#include <wx/sizer.h>
#include <algorithm>
namespace ccm::ui {
wxDEFINE_EVENT(EVT_VARIANT_PREVIEW_PREV, wxCommandEvent);
wxDEFINE_EVENT(EVT_VARIANT_PREVIEW_NEXT, wxCommandEvent);
class VariantImagePreviewDialog::ImageCanvas : public wxPanel {
public:
explicit ImageCanvas(wxWindow* parent) : wxPanel(parent, wxID_ANY) {
SetBackgroundStyle(wxBG_STYLE_PAINT);
Bind(wxEVT_PAINT, &ImageCanvas::onPaint, this);
Bind(wxEVT_SIZE, [this](wxSizeEvent& ev) {
Refresh();
ev.Skip();
});
}
void setImage(const wxImage& img) {
original_ = img;
cachedScaled_ = wxBitmap();
cachedScaledFor_ = wxSize(-1, -1);
Refresh();
}
void clear() {
original_ = wxImage();
cachedScaled_ = wxBitmap();
cachedScaledFor_ = wxSize(-1, -1);
Refresh();
}
private:
void onPaint(wxPaintEvent&) {
wxPaintDC dc(this);
dc.Clear();
if (!original_.IsOk()) return;
const wxSize ws = GetClientSize();
if (ws.GetWidth() <= 0 || ws.GetHeight() <= 0) return;
const double scale = std::min(
static_cast<double>(ws.GetWidth()) / original_.GetWidth(),
static_cast<double>(ws.GetHeight()) / original_.GetHeight());
const int w = std::max(1, static_cast<int>(original_.GetWidth() * scale));
const int h = std::max(1, static_cast<int>(original_.GetHeight() * scale));
const wxSize scaledSize(w, h);
if (!cachedScaled_.IsOk() || cachedScaledFor_ != scaledSize) {
wxImage scaled = original_.Scale(w, h, wxIMAGE_QUALITY_HIGH);
cachedScaled_ = wxBitmap(scaled);
cachedScaledFor_ = scaledSize;
}
dc.DrawBitmap(cachedScaled_,
(ws.GetWidth() - w) / 2,
(ws.GetHeight() - h) / 2,
true);
}
wxImage original_;
wxBitmap cachedScaled_;
wxSize cachedScaledFor_{-1, -1};
};
VariantImagePreviewDialog::VariantImagePreviewDialog(wxWindow* parent)
: wxDialog(parent, wxID_ANY, "Print preview",
wxDefaultPosition, wxSize(280, 420),
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxSTAY_ON_TOP) {
auto* root = new wxBoxSizer(wxVERTICAL);
imageHost_ = new ImageCanvas(this);
root->Add(imageHost_, 1, wxEXPAND | wxALL, 6);
caption_ = new wxStaticText(this, wxID_ANY, "");
root->Add(caption_, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 6);
auto* nav = new wxBoxSizer(wxHORIZONTAL);
prevButton_ = new wxButton(this, wxID_ANY, "<< Prev");
nextButton_ = new wxButton(this, wxID_ANY, "Next >>");
nav->Add(prevButton_, 0, wxRIGHT, 6);
nav->Add(nextButton_, 0);
root->Add(nav, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxBOTTOM, 6);
prevButton_->Bind(wxEVT_BUTTON, &VariantImagePreviewDialog::onPrev, this);
nextButton_->Bind(wxEVT_BUTTON, &VariantImagePreviewDialog::onNext, this);
SetSizer(root);
Layout();
repositionBesideParent();
}
void VariantImagePreviewDialog::repositionBesideParent() {
wxWindow* parent = GetParent();
if (parent == nullptr) return;
const wxRect parentScreen = parent->GetScreenRect();
const wxSize size = GetSize();
int x = parentScreen.GetRight() + 20;
int y = parentScreen.GetTop();
const int displayIdx = wxDisplay::GetFromWindow(parent);
if (displayIdx != wxNOT_FOUND) {
const wxRect work = wxDisplay(displayIdx).GetClientArea();
if (x + size.GetWidth() > work.GetRight()) {
x = std::max(work.GetLeft(), work.GetRight() - size.GetWidth());
}
if (y + size.GetHeight() > work.GetBottom()) {
y = std::max(work.GetTop(), work.GetBottom() - size.GetHeight());
}
if (x < work.GetLeft()) x = work.GetLeft();
if (y < work.GetTop()) y = work.GetTop();
}
SetPosition(wxPoint(x, y));
}
void VariantImagePreviewDialog::setCaption(const wxString& caption) {
if (caption_) {
caption_->SetLabelText(caption);
Layout();
}
}
void VariantImagePreviewDialog::setImageBytes(std::string_view bytes) {
if (!imageHost_) return;
if (bytes.empty()) {
clearImage();
return;
}
wxMemoryInputStream stream(bytes.data(), bytes.size());
wxImage img;
bool decoded = false;
{
wxLogNull suppressPngWarnings;
decoded = img.LoadFile(stream, wxBITMAP_TYPE_ANY);
}
if (!decoded || !img.IsOk()) {
clearImage();
return;
}
imageHost_->setImage(img);
}
void VariantImagePreviewDialog::clearImage() {
if (imageHost_) {
imageHost_->clear();
}
}
void VariantImagePreviewDialog::setNavigationEnabled(bool enabled) {
if (prevButton_) prevButton_->Enable(enabled);
if (nextButton_) nextButton_->Enable(enabled);
}
void VariantImagePreviewDialog::onPrev(wxCommandEvent&) {
wxCommandEvent ev(EVT_VARIANT_PREVIEW_PREV, GetId());
ev.SetEventObject(this);
ProcessWindowEvent(ev);
}
void VariantImagePreviewDialog::onNext(wxCommandEvent&) {
wxCommandEvent ev(EVT_VARIANT_PREVIEW_NEXT, GetId());
ev.SetEventObject(this);
ProcessWindowEvent(ev);
}
} // namespace ccm::ui