mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 17:01:02 +00:00
minor: yugioh support added
This commit is contained in:
@@ -25,6 +25,7 @@ struct AppContext {
|
||||
CardPreviewService& cardPreview;
|
||||
IGameModule& magicModule;
|
||||
IGameModule& pokemonModule;
|
||||
IGameModule& yuGiOhModule;
|
||||
// Active per-game UI bundles. The order is the order shown in the
|
||||
// Game menu; the composition root constructs them and hands raw
|
||||
// pointers in. `MainFrame` does not own these — `app/main.cpp` does.
|
||||
|
||||
@@ -90,6 +90,7 @@ protected:
|
||||
}
|
||||
buildLayout();
|
||||
populateChoices();
|
||||
openingSnapshot_ = card_;
|
||||
Thaw();
|
||||
}
|
||||
|
||||
@@ -119,6 +120,11 @@ protected:
|
||||
return "(no sets cached - use Sets > " + updateMenuName() + ")";
|
||||
}
|
||||
|
||||
// Called when the card name field changes. Games that cache upstream print
|
||||
// metadata keyed by `(name, set)` should clear it here so stale "Next"
|
||||
// controls cannot outlive the lookup identity.
|
||||
virtual void onCardLookupContextChanged() {}
|
||||
|
||||
// Common helpers ----------------------------------------------------------
|
||||
|
||||
void appendRow(wxFlexGridSizer* grid, const wxString& label, wxWindow* ctrl) {
|
||||
@@ -129,12 +135,26 @@ protected:
|
||||
|
||||
[[nodiscard]] TCard& mutableCard() noexcept { return card_; }
|
||||
[[nodiscard]] const TCard& constCard() const noexcept { return card_; }
|
||||
void syncCardFromControls() { writeFromControls(); }
|
||||
[[nodiscard]] wxComboBox* setComboControl() const noexcept { return setCombo_; }
|
||||
|
||||
[[nodiscard]] const Set* selectedSetFromControls() const {
|
||||
const auto& available = availableSets();
|
||||
if (!setCombo_ || !setCombo_->IsEnabled()) return nullptr;
|
||||
const int sel = setCombo_->GetSelection();
|
||||
if (sel < 0 || static_cast<std::size_t>(sel) >= available.size()) return nullptr;
|
||||
return &available[static_cast<std::size_t>(sel)];
|
||||
}
|
||||
|
||||
private:
|
||||
void readSets() {
|
||||
auto loaded = setService_.getSets(game_);
|
||||
if (loaded.isOk()) {
|
||||
sets_ = std::move(loaded).value();
|
||||
std::sort(sets_.begin(), sets_.end(),
|
||||
[](const Set& a, const Set& b) {
|
||||
return a.releaseDate < b.releaseDate;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +168,10 @@ private:
|
||||
grid->AddGrowableCol(1, 1);
|
||||
|
||||
nameCtrl_ = new wxTextCtrl(this, wxID_ANY, wxString::FromUTF8(card_.name.c_str()));
|
||||
nameCtrl_->Bind(wxEVT_TEXT, [this](wxCommandEvent& ev) {
|
||||
onCardLookupContextChanged();
|
||||
ev.Skip();
|
||||
});
|
||||
appendRow(grid, "Name", nameCtrl_);
|
||||
|
||||
setCombo_ = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0,
|
||||
@@ -393,6 +417,14 @@ private:
|
||||
"Add card", wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
if (mode_ == EditMode::Edit && !(card_ == openingSnapshot_)) {
|
||||
if (showThemedConfirmDialog(
|
||||
this,
|
||||
wxString::FromUTF8("Save changes to this card?"),
|
||||
wxString::FromUTF8("Save changes")) != wxID_YES) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ev.Skip();
|
||||
}
|
||||
|
||||
@@ -503,6 +535,7 @@ private:
|
||||
SetService& setService_;
|
||||
EditMode mode_;
|
||||
TCard card_;
|
||||
TCard openingSnapshot_{};
|
||||
Game game_;
|
||||
|
||||
std::vector<Set> sets_;
|
||||
|
||||
@@ -30,9 +30,12 @@
|
||||
#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>
|
||||
@@ -43,8 +46,10 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
@@ -221,6 +226,9 @@ protected:
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -284,6 +292,9 @@ private:
|
||||
case Game::Pokemon:
|
||||
// Mirrors CCM2's unresolved-preview fallback image.
|
||||
return "https://archives.bulbagarden.net/media/upload/1/17/Cardback.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";
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -367,25 +378,58 @@ private:
|
||||
CardPreviewService* svcPtr = &cardPreview_;
|
||||
auto [name, setId, setNo] = previewKey(card);
|
||||
const Game game = gameId();
|
||||
const std::string exeDirCopy = exeDirForBundledAssets_;
|
||||
|
||||
std::thread([state, gen, svcPtr, name = std::move(name),
|
||||
setId = std::move(setId), setNo = std::move(setNo), game]() {
|
||||
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()) {
|
||||
const std::string fallbackUrl = fallbackImageUrlForGame(game);
|
||||
if (!fallbackUrl.empty()) {
|
||||
auto fallbackBytes = svcPtr->fetchImageBytesByUrl(fallbackUrl);
|
||||
if (fallbackBytes.isOk()) {
|
||||
payload = std::move(fallbackBytes).value();
|
||||
ok = !payload.empty();
|
||||
if (ok) {
|
||||
usedFallback = true;
|
||||
err.clear();
|
||||
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 {
|
||||
const std::string fallbackUrl = fallbackImageUrlForGame(game);
|
||||
if (!fallbackUrl.empty()) {
|
||||
auto fallbackBytes = svcPtr->fetchImageBytesByUrl(fallbackUrl);
|
||||
if (fallbackBytes.isOk()) {
|
||||
applyFallbackPayload(std::move(fallbackBytes).value());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,7 +463,16 @@ private:
|
||||
|
||||
wxMemoryInputStream stream(bytes.data(), bytes.size());
|
||||
wxImage img;
|
||||
if (!img.LoadFile(stream, wxBITMAP_TYPE_ANY)) {
|
||||
// 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();
|
||||
@@ -477,6 +530,7 @@ private:
|
||||
|
||||
ImageService& imageService_;
|
||||
CardPreviewService& cardPreview_;
|
||||
std::string exeDirForBundledAssets_;
|
||||
std::optional<TCard> card_;
|
||||
|
||||
wxStaticBitmap* previewBitmap_{nullptr};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/services/CardPreviewService.hpp"
|
||||
#include "ccm/ui/BaseCardEditDialog.hpp"
|
||||
#include <wx/button.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhCardEditDialog final : public BaseCardEditDialog<YuGiOhCard> {
|
||||
public:
|
||||
YuGiOhCardEditDialog(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
SetService& setService,
|
||||
CardPreviewService& cardPreview,
|
||||
EditMode mode,
|
||||
YuGiOhCard initial,
|
||||
const std::vector<Set>* preloadedSets = nullptr);
|
||||
|
||||
protected:
|
||||
void buildFlagsRow(wxBoxSizer* flagsBox) override;
|
||||
void appendExtraRows(wxFlexGridSizer* grid) override;
|
||||
void readExtraFromCard() override;
|
||||
void writeExtraToCard() override;
|
||||
[[nodiscard]] std::string updateMenuName() const override { return "Update Yu-Gi-Oh!"; }
|
||||
void onCardLookupContextChanged() override;
|
||||
|
||||
private:
|
||||
void onAutoDetectSetNo(wxCommandEvent&);
|
||||
void onAutoDetectRarity(wxCommandEvent&);
|
||||
void onNextSetNo(wxCommandEvent&);
|
||||
void onNextRarity(wxCommandEvent&);
|
||||
void onSetNoTextChanged(wxCommandEvent&);
|
||||
void onSetSelectionChanged(wxCommandEvent&);
|
||||
void autoDetectFromApi(bool fillSetNo, bool fillRarity);
|
||||
void refreshSetNoFullPreview();
|
||||
void clearCachedPrintVariants();
|
||||
bool fetchAndCachePrintVariants();
|
||||
void rebuildVariantRingsFromCache();
|
||||
void filterCachedVariantsForCardLanguage();
|
||||
void syncRingPositionsToControls();
|
||||
void applyRarityStringToChoice(const std::string& rarity);
|
||||
[[nodiscard]] std::string currentFullSetNoFromControls() const;
|
||||
void refreshVariantNextControls();
|
||||
[[nodiscard]] std::string extractSetNoNumeric(std::string_view fullSetNo) const;
|
||||
[[nodiscard]] std::string composeFullSetNo(std::string_view numeric) const;
|
||||
void scheduleDeferredVariantPrefetch();
|
||||
void prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch);
|
||||
|
||||
EditMode dialogMode_;
|
||||
unsigned variantFetchEpoch_{0};
|
||||
CardPreviewService& cardPreview_;
|
||||
wxTextCtrl* setNoCtrl_{nullptr};
|
||||
wxStaticText* setNoFullPreview_{nullptr};
|
||||
wxChoice* rarityChoice_{nullptr};
|
||||
wxButton* autoSetNoBtn_{nullptr};
|
||||
wxButton* nextSetNoBtn_{nullptr};
|
||||
wxButton* autoRarityBtn_{nullptr};
|
||||
wxButton* nextRarityBtn_{nullptr};
|
||||
wxCheckBox* firstEditionCheck_{nullptr};
|
||||
wxCheckBox* signedCheck_{nullptr};
|
||||
wxCheckBox* alteredCheck_{nullptr};
|
||||
|
||||
std::vector<AutoDetectedPrint> cachedVariants_;
|
||||
std::vector<std::string> uniqueSetCodes_;
|
||||
std::vector<std::string> raritiesForCurrentSetCode_;
|
||||
std::size_t setCodeRingPos_{0};
|
||||
std::size_t rarityRingPos_{0};
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
#include "ccm/services/CardSorter.hpp"
|
||||
#include "ccm/ui/BaseCardListPanel.hpp"
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhCardListPanel final : public BaseCardListPanel<YuGiOhCard, YuGiOhSortColumn> {
|
||||
public:
|
||||
explicit YuGiOhCardListPanel(wxWindow* parent);
|
||||
|
||||
protected:
|
||||
[[nodiscard]] std::vector<TextColumnSpec> declareTextColumns() const override;
|
||||
[[nodiscard]] std::vector<IconColumnSpec> declareIconColumns() const override;
|
||||
[[nodiscard]] std::string renderTextCell(const YuGiOhCard& card, std::size_t idx) const override;
|
||||
[[nodiscard]] bool isIconColumnSet(const YuGiOhCard& card, std::size_t idx) const override;
|
||||
void sortBy(YuGiOhSortColumn column, bool ascending) override;
|
||||
[[nodiscard]] bool matchesFilter(const YuGiOhCard& card, std::string_view filter) const override;
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/services/CardPreviewService.hpp"
|
||||
#include "ccm/services/CollectionService.hpp"
|
||||
#include "ccm/services/ConfigService.hpp"
|
||||
#include "ccm/services/ImageService.hpp"
|
||||
#include "ccm/services/SetService.hpp"
|
||||
#include "ccm/ui/IGameView.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhCardListPanel;
|
||||
class YuGiOhSelectedCardPanel;
|
||||
|
||||
class YuGiOhGameView final : public IGameView {
|
||||
public:
|
||||
YuGiOhGameView(ConfigService& config,
|
||||
CollectionService<YuGiOhCard>& collection,
|
||||
SetService& sets,
|
||||
ImageService& images,
|
||||
CardPreviewService& cardPreview,
|
||||
IGameModule& module);
|
||||
|
||||
[[nodiscard]] Game gameId() const noexcept override { return Game::YuGiOh; }
|
||||
[[nodiscard]] std::string displayName() const override { return "Yu-Gi-Oh!"; }
|
||||
|
||||
wxPanel* listPanel(wxWindow* parent) override;
|
||||
wxPanel* selectedPanel(wxWindow* parent) override;
|
||||
|
||||
void refreshCollection() override;
|
||||
void onAddCard(wxWindow* parentWindow) override;
|
||||
void onEditCard(wxWindow* parentWindow) override;
|
||||
void onDeleteCard(wxWindow* parentWindow) override;
|
||||
std::string onUpdateSets(wxWindow* parentWindow) override;
|
||||
void setFilter(std::string_view filter) override;
|
||||
void applyTheme(const ThemePalette& palette) override;
|
||||
[[nodiscard]] std::string updateSetsMenuLabel() const override { return "Update Yu-Gi-Oh!"; }
|
||||
|
||||
private:
|
||||
void ensureSetsLoaded();
|
||||
const std::vector<Set>& setsForDialog();
|
||||
|
||||
ConfigService& config_;
|
||||
CollectionService<YuGiOhCard>& collection_;
|
||||
SetService& sets_;
|
||||
ImageService& images_;
|
||||
CardPreviewService& cardPreview_;
|
||||
IGameModule& module_;
|
||||
|
||||
YuGiOhCardListPanel* listPanel_{nullptr};
|
||||
YuGiOhSelectedCardPanel* selectedPanel_{nullptr};
|
||||
std::vector<Set> setsCache_;
|
||||
bool attemptedInitialSetLoad_{false};
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
#include "ccm/ui/BaseSelectedCardPanel.hpp"
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhSelectedCardPanel final : public BaseSelectedCardPanel<YuGiOhCard> {
|
||||
public:
|
||||
YuGiOhSelectedCardPanel(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
CardPreviewService& cardPreview);
|
||||
|
||||
protected:
|
||||
[[nodiscard]] std::vector<DetailRowSpec> declareDetailRows() const override;
|
||||
[[nodiscard]] std::vector<FlagIconSpec> declareFlagIcons() const override;
|
||||
[[nodiscard]] std::string detailValueFor(const YuGiOhCard& card, DetailKey key) const override;
|
||||
[[nodiscard]] bool isFlagSet(const YuGiOhCard& card, DetailKey key) const override;
|
||||
[[nodiscard]] std::tuple<std::string, std::string, std::string>
|
||||
previewKey(const YuGiOhCard& card) const override;
|
||||
[[nodiscard]] Game gameId() const noexcept override { return Game::YuGiOh; }
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
Reference in New Issue
Block a user