mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-09-04 01:03:27 +00:00
Minor: Add Asian Pokemon Card Support (#18)
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user