mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-29 03:01:16 +00:00
patch: Feature/pkm autodetect (#15)
This commit is contained in:
@@ -7,7 +7,15 @@
|
||||
// - `Holo`, `1. Edition`, `Signed`, `Altered` check boxes in the flags row
|
||||
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/services/CardPreviewService.hpp"
|
||||
#include "ccm/ui/BaseCardEditDialog.hpp"
|
||||
#include <wx/button.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
@@ -16,9 +24,11 @@ public:
|
||||
PokemonCardEditDialog(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
SetService& setService,
|
||||
CardPreviewService& cardPreview,
|
||||
EditMode mode,
|
||||
PokemonCard initial,
|
||||
const std::vector<Set>* preloadedSets = nullptr);
|
||||
~PokemonCardEditDialog() override;
|
||||
|
||||
protected:
|
||||
void buildFlagsRow(wxBoxSizer* flagsBox) override;
|
||||
@@ -26,13 +36,50 @@ protected:
|
||||
void readExtraFromCard() override;
|
||||
void writeExtraToCard() override;
|
||||
[[nodiscard]] std::string updateMenuName() const override { return "Update Pokemon"; }
|
||||
void onCardLookupContextChanged() override;
|
||||
|
||||
private:
|
||||
wxTextCtrl* setNoCtrl_{nullptr};
|
||||
wxCheckBox* holoCheck_{nullptr};
|
||||
wxCheckBox* firstEditionCheck_{nullptr};
|
||||
wxCheckBox* signedCheck_{nullptr};
|
||||
wxCheckBox* alteredCheck_{nullptr};
|
||||
struct VariantFetchState {
|
||||
std::atomic<bool> alive{true};
|
||||
};
|
||||
|
||||
void onAutoDetectSetNo(wxCommandEvent&);
|
||||
void onNextSetNo(wxCommandEvent&);
|
||||
void onSetSelectionChanged(wxCommandEvent&);
|
||||
void autoDetectFromApi();
|
||||
void clearCachedPrintVariants();
|
||||
void requestVariantsAsync(unsigned capturedEpoch,
|
||||
std::string name,
|
||||
std::string setId,
|
||||
bool fillSetNoOnSuccess,
|
||||
bool showFailureDialog);
|
||||
void applyDetectedVariants(unsigned capturedEpoch,
|
||||
Result<std::vector<AutoDetectedPrint>> detected,
|
||||
bool fillSetNoOnSuccess,
|
||||
bool showFailureDialog);
|
||||
void rebuildVariantRingFromCache();
|
||||
void syncRingPositionToControls();
|
||||
void refreshVariantNextControls();
|
||||
void scheduleDeferredVariantPrefetch();
|
||||
void prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch);
|
||||
[[nodiscard]] static std::string storedSetNoFromControls(const wxTextCtrl* ctrl);
|
||||
[[nodiscard]] static std::string normalizedStoredSetNo(std::string_view setNo);
|
||||
|
||||
EditMode dialogMode_;
|
||||
unsigned variantFetchEpoch_{0};
|
||||
CardPreviewService& cardPreview_;
|
||||
std::shared_ptr<VariantFetchState> variantFetchState_;
|
||||
wxTextCtrl* setNoCtrl_{nullptr};
|
||||
wxButton* autoSetNoBtn_{nullptr};
|
||||
wxButton* nextSetNoBtn_{nullptr};
|
||||
wxCheckBox* holoCheck_{nullptr};
|
||||
wxCheckBox* firstEditionCheck_{nullptr};
|
||||
wxCheckBox* signedCheck_{nullptr};
|
||||
wxCheckBox* alteredCheck_{nullptr};
|
||||
|
||||
std::vector<AutoDetectedPrint> cachedVariants_;
|
||||
std::vector<std::string> uniqueSetNos_;
|
||||
std::size_t setNoRingPos_{0};
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
#include "ccm/ui/PokemonCardEditDialog.hpp"
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include <wx/app.h>
|
||||
#include <wx/panel.h>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
PokemonCardEditDialog::PokemonCardEditDialog(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
SetService& setService,
|
||||
CardPreviewService& cardPreview,
|
||||
EditMode mode,
|
||||
PokemonCard initial,
|
||||
const std::vector<Set>* preloadedSets)
|
||||
: BaseCardEditDialog<PokemonCard>(
|
||||
parent,
|
||||
mode == EditMode::Create ? "Add Pokemon Card" : "Edit Pokemon Card",
|
||||
imageService, setService, mode, std::move(initial), Game::Pokemon, preloadedSets) {
|
||||
imageService, setService, mode, std::move(initial), Game::Pokemon, preloadedSets),
|
||||
dialogMode_(mode),
|
||||
cardPreview_(cardPreview),
|
||||
variantFetchState_(std::make_shared<VariantFetchState>()) {
|
||||
buildAndPopulate();
|
||||
if (dialogMode_ == EditMode::Edit) {
|
||||
scheduleDeferredVariantPrefetch();
|
||||
}
|
||||
}
|
||||
|
||||
PokemonCardEditDialog::~PokemonCardEditDialog() {
|
||||
if (variantFetchState_) {
|
||||
variantFetchState_->alive.store(false);
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::onCardLookupContextChanged() {
|
||||
clearCachedPrintVariants();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
|
||||
@@ -27,12 +50,46 @@ void PokemonCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::appendExtraRows(wxFlexGridSizer* grid) {
|
||||
setNoCtrl_ = new wxTextCtrl(this, wxID_ANY, constCard().setNo);
|
||||
appendRow(grid, "Set #", setNoCtrl_);
|
||||
auto* setNoPanel = new wxPanel(this, wxID_ANY);
|
||||
setNoCtrl_ = new wxTextCtrl(setNoPanel, wxID_ANY);
|
||||
autoSetNoBtn_ = new wxButton(setNoPanel, wxID_ANY, "Auto detect");
|
||||
autoSetNoBtn_->Bind(wxEVT_BUTTON, &PokemonCardEditDialog::onAutoDetectSetNo, this);
|
||||
nextSetNoBtn_ = new wxButton(setNoPanel, wxID_ANY, "Next");
|
||||
nextSetNoBtn_->Bind(wxEVT_BUTTON, &PokemonCardEditDialog::onNextSetNo, this);
|
||||
nextSetNoBtn_->Show(false);
|
||||
auto* setNoRow = new wxBoxSizer(wxHORIZONTAL);
|
||||
setNoRow->Add(setNoCtrl_, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
setNoRow->Add(autoSetNoBtn_, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
setNoRow->Add(nextSetNoBtn_, 0, wxALIGN_CENTER_VERTICAL);
|
||||
setNoPanel->SetSizer(setNoRow);
|
||||
|
||||
appendRow(grid, "Set #", setNoPanel);
|
||||
|
||||
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);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string PokemonCardEditDialog::storedSetNoFromControls(const wxTextCtrl* ctrl) {
|
||||
if (ctrl == nullptr) return {};
|
||||
return normalizedStoredSetNo(ctrl->GetValue().ToStdString(wxConvUTF8));
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::readExtraFromCard() {
|
||||
if (setNoCtrl_) setNoCtrl_->ChangeValue(constCard().setNo);
|
||||
clearCachedPrintVariants();
|
||||
if (setNoCtrl_) {
|
||||
setNoCtrl_->ChangeValue(
|
||||
wxString::FromUTF8(normalizedStoredSetNo(constCard().setNo).c_str()));
|
||||
}
|
||||
if (holoCheck_) holoCheck_->SetValue(constCard().holo);
|
||||
if (firstEditionCheck_) firstEditionCheck_->SetValue(constCard().firstEdition);
|
||||
if (signedCheck_) signedCheck_->SetValue(constCard().signed_);
|
||||
@@ -40,11 +97,159 @@ void PokemonCardEditDialog::readExtraFromCard() {
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::writeExtraToCard() {
|
||||
if (setNoCtrl_) mutableCard().setNo = setNoCtrl_->GetValue().ToStdString();
|
||||
if (setNoCtrl_) mutableCard().setNo = storedSetNoFromControls(setNoCtrl_);
|
||||
if (holoCheck_) mutableCard().holo = holoCheck_->IsChecked();
|
||||
if (firstEditionCheck_) mutableCard().firstEdition = firstEditionCheck_->IsChecked();
|
||||
if (signedCheck_) mutableCard().signed_ = signedCheck_->IsChecked();
|
||||
if (alteredCheck_) mutableCard().altered = alteredCheck_->IsChecked();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::clearCachedPrintVariants() {
|
||||
++variantFetchEpoch_;
|
||||
cachedVariants_.clear();
|
||||
uniqueSetNos_.clear();
|
||||
setNoRingPos_ = 0;
|
||||
refreshVariantNextControls();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::scheduleDeferredVariantPrefetch() {
|
||||
const unsigned epoch = variantFetchEpoch_;
|
||||
wxTheApp->CallAfter([this, epoch]() {
|
||||
prefetchVariantsForCurrentCardSilent(epoch);
|
||||
});
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch) {
|
||||
if (capturedEpoch != variantFetchEpoch_) return;
|
||||
if (!cachedVariants_.empty()) return;
|
||||
const auto& card = constCard();
|
||||
if (card.name.empty() || card.set.id.empty()) return;
|
||||
|
||||
requestVariantsAsync(capturedEpoch, card.name, card.set.id, false, false);
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::requestVariantsAsync(unsigned capturedEpoch,
|
||||
std::string name,
|
||||
std::string setId,
|
||||
bool fillSetNoOnSuccess,
|
||||
bool showFailureDialog) {
|
||||
if (capturedEpoch != variantFetchEpoch_) return;
|
||||
|
||||
if (fillSetNoOnSuccess && autoSetNoBtn_) {
|
||||
autoSetNoBtn_->Disable();
|
||||
}
|
||||
|
||||
auto state = variantFetchState_;
|
||||
CardPreviewService* svc = &cardPreview_;
|
||||
PokemonCardEditDialog* self = this;
|
||||
std::thread([state, svc, self, capturedEpoch, name = std::move(name),
|
||||
setId = std::move(setId), fillSetNoOnSuccess, showFailureDialog]() {
|
||||
auto detected = svc->detectPrintVariants(Game::Pokemon, name, setId);
|
||||
wxTheApp->CallAfter([state, self, capturedEpoch, detected = std::move(detected),
|
||||
fillSetNoOnSuccess, showFailureDialog]() mutable {
|
||||
if (!state->alive.load()) return;
|
||||
self->applyDetectedVariants(capturedEpoch, std::move(detected),
|
||||
fillSetNoOnSuccess, showFailureDialog);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::applyDetectedVariants(unsigned capturedEpoch,
|
||||
Result<std::vector<AutoDetectedPrint>> detected,
|
||||
bool fillSetNoOnSuccess,
|
||||
bool showFailureDialog) {
|
||||
if (capturedEpoch != variantFetchEpoch_) return;
|
||||
|
||||
if (fillSetNoOnSuccess && autoSetNoBtn_) {
|
||||
autoSetNoBtn_->Enable();
|
||||
}
|
||||
|
||||
if (!detected) {
|
||||
if (showFailureDialog) {
|
||||
showThemedMessageDialog(this, "Auto detect failed: " + detected.error(), "Auto detect",
|
||||
wxOK | wxICON_WARNING);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cachedVariants_ = std::move(detected).value();
|
||||
if (fillSetNoOnSuccess && setNoCtrl_ && !cachedVariants_.empty()) {
|
||||
setNoCtrl_->ChangeValue(
|
||||
wxString::FromUTF8(cachedVariants_.front().setNo.c_str()));
|
||||
}
|
||||
|
||||
rebuildVariantRingFromCache();
|
||||
syncRingPositionToControls();
|
||||
refreshVariantNextControls();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::rebuildVariantRingFromCache() {
|
||||
uniqueSetNos_.clear();
|
||||
if (cachedVariants_.empty()) return;
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
seen.reserve(cachedVariants_.size());
|
||||
for (const auto& p : cachedVariants_) {
|
||||
if (p.setNo.empty()) continue;
|
||||
if (!seen.insert(p.setNo).second) continue;
|
||||
uniqueSetNos_.push_back(p.setNo);
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::syncRingPositionToControls() {
|
||||
if (!setNoCtrl_) return;
|
||||
const std::string current = storedSetNoFromControls(setNoCtrl_);
|
||||
setNoRingPos_ = 0;
|
||||
for (std::size_t i = 0; i < uniqueSetNos_.size(); ++i) {
|
||||
if (uniqueSetNos_[i] == current) {
|
||||
setNoRingPos_ = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::refreshVariantNextControls() {
|
||||
if (!nextSetNoBtn_) return;
|
||||
nextSetNoBtn_->Show(uniqueSetNos_.size() > 1);
|
||||
Layout();
|
||||
if (GetSizer()) Fit();
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
refreshVariantNextControls();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::autoDetectFromApi() {
|
||||
syncCardFromControls();
|
||||
const auto& card = constCard();
|
||||
if (card.name.empty()) {
|
||||
showThemedMessageDialog(this, "Enter a card name first.", "Auto detect",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
if (card.set.id.empty()) {
|
||||
showThemedMessageDialog(this, "Select a set first.", "Auto detect",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned epoch = variantFetchEpoch_;
|
||||
requestVariantsAsync(epoch, card.name, card.set.id, true, true);
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::onSetSelectionChanged(wxCommandEvent& ev) {
|
||||
clearCachedPrintVariants();
|
||||
scheduleDeferredVariantPrefetch();
|
||||
ev.Skip();
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -90,7 +90,7 @@ void PokemonGameView::onAddCard(wxWindow* parentWindow) {
|
||||
fresh.language = Language::English;
|
||||
fresh.condition = Condition::NearMint;
|
||||
|
||||
PokemonCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Create, fresh,
|
||||
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Create, fresh,
|
||||
&setsForDialog());
|
||||
themeModalDialog(&dlg, config_.current().theme);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
@@ -129,7 +129,7 @@ void PokemonGameView::onEditCard(wxWindow* parentWindow) {
|
||||
showThemedMessageDialog(parentWindow, "Select a card first.", "Edit", wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
PokemonCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Edit, *sel,
|
||||
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Edit, *sel,
|
||||
&setsForDialog());
|
||||
themeModalDialog(&dlg, config_.current().theme);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
|
||||
Reference in New Issue
Block a user