mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-29 01:08:49 +00:00
major: initial release
* initial development * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * pipeline * ci/cd * ci/cd * ci/cd * ci/cd * ci/cd * ci/cd * ci/cd * pokemon * pokemon * pokemon * pokemon * pokemon * pokemon * improvements * improvements * ci/cd * ci/cd * improvements * improvements * improvements * improvements * improvements * improvements * improvements * improvements * improvements --------- Co-authored-by: sdine <sdine@sdine.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
// CardFilter - row matcher aligned with TableTemplate.tsx::applyFilter semantics.
|
||||
// kept a single `filter` string in the table component and, before rendering,
|
||||
// kept only the rows where *any* of the `tableFields` value-key columns
|
||||
// (string- or number-typed) contained the filter as a substring. Boolean flag
|
||||
// columns (foil / signed / altered / holo / firstEdition) were skipped because
|
||||
// `typeof` was neither "string" nor "number".
|
||||
//
|
||||
// We replicate that logic here as free functions so the UI can stay dumb (it
|
||||
// just owns a wxTextCtrl and re-asks core which rows match).
|
||||
//
|
||||
// Differences worth knowing:
|
||||
// * The match is case-insensitive on **both** sides — the old JS path only lowercased
|
||||
// the cell value, leaving the filter as-typed, which made uppercase input
|
||||
// never match. Lowercasing the filter too is the obvious-intent fix and
|
||||
// keeps round-trip semantics for any filter the original UI would accept
|
||||
// (lowercase filters behave identically).
|
||||
// * An empty filter matches every row, exactly as in JS where every string
|
||||
// `.includes("")` returns true.
|
||||
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
// Magic value-key columns from the MtgTable.tsx tableFields list:
|
||||
// name, set.name, language, condition, amount, note.
|
||||
// Foil/Signed/Altered are bool-typed and intentionally excluded.
|
||||
[[nodiscard]] bool matchesMagicFilter(const MagicCard& card,
|
||||
std::string_view filter);
|
||||
|
||||
// Pokemon value-key columns from PokemonTable.tsx tableFields list:
|
||||
// name, set.name, setNo, language, condition, amount, note.
|
||||
// Holo/FirstEdition/Signed/Altered are bool-typed and excluded.
|
||||
[[nodiscard]] bool matchesPokemonFilter(const PokemonCard& card,
|
||||
std::string_view filter);
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
// CardPreviewService: high-level operation for resolving and downloading a
|
||||
// card's preview image from the active game's external API. Mirrors the
|
||||
// `EntryPanelTemplate.tsx::useEffect([entry])` flow:
|
||||
// 1. Per-game `ICardPreviewSource` resolves the card -> an image URL.
|
||||
// 2. Service issues a GET against that URL through `IHttpClient`.
|
||||
// 3. Raw bytes are returned to the caller (UI decodes them with whichever
|
||||
// image lib it prefers - we use wxImage in the wx adapter).
|
||||
//
|
||||
// Sources are registered through `IGameModule::cardPreviewSource()`. Modules
|
||||
// that return nullptr are silently skipped; games without a registered source
|
||||
// produce an explicit error result on lookup rather than silently doing nothing.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/ports/IHttpClient.hpp"
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class CardPreviewService {
|
||||
public:
|
||||
explicit CardPreviewService(IHttpClient& http);
|
||||
|
||||
// Register a game module's preview source. Calling this with a module
|
||||
// whose `cardPreviewSource()` returns nullptr is a no-op (the game has
|
||||
// no remote preview API). The module reference must remain valid for
|
||||
// the lifetime of the service.
|
||||
void registerModule(IGameModule& module);
|
||||
|
||||
// Resolve and download the preview image bytes for a single card.
|
||||
// The returned `std::string` is a raw byte buffer (PNG/JPEG payload) -
|
||||
// it is NOT decoded text. Use std::string::data()/size() with whatever
|
||||
// image-decoding facility your UI provides.
|
||||
Result<std::string> fetchPreviewBytes(Game game,
|
||||
std::string_view name,
|
||||
std::string_view setId,
|
||||
std::string_view setNo);
|
||||
|
||||
// Download image bytes from a fully-qualified URL without going through
|
||||
// per-game preview-source resolution.
|
||||
Result<std::string> fetchImageBytesByUrl(std::string_view url);
|
||||
|
||||
private:
|
||||
IHttpClient& http_;
|
||||
std::unordered_map<Game, ICardPreviewSource*> sources_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
// CardSorter - per-column sorting behavior matching the established table model
|
||||
// `tableFields` configuration. The original TS code stored each column's
|
||||
// (valueKey, sortKey) pair on a config object so that, e.g., the "Set" column
|
||||
// could *display* `set.name` while sorting by `set.releaseDate`. We replicate
|
||||
// that mapping here as two enums (one per supported game), each variant naming
|
||||
// a sort key. The free functions below run a stable sort over a vector using
|
||||
// the same per-type rules as the original `byField`:
|
||||
//
|
||||
// * strings -> case-insensitive (lowercased) `<` / `>`
|
||||
// * numbers -> direct `<` / `>`
|
||||
// * booleans -> direct `<` / `>` (so false < true; unset flags first when asc)
|
||||
//
|
||||
// Stable sort matches the JS `Array.prototype.sort` guarantee from ES2019; the
|
||||
// UI relies on it so successive clicks on different columns compose predictably
|
||||
// (e.g. sort by name, then by set => grouped by set, name-sorted within each).
|
||||
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
// One value per *real* (non-spacer) column of the Magic table, in the same
|
||||
// order as the Magic tableFields list.
|
||||
enum class MagicSortColumn {
|
||||
Name,
|
||||
SetReleaseDate, // value column shows set.name, sort key is set.releaseDate
|
||||
Language,
|
||||
Condition,
|
||||
Amount,
|
||||
Foil,
|
||||
Signed,
|
||||
Altered,
|
||||
Note,
|
||||
};
|
||||
|
||||
// Pokemon equivalent (PokemonTable.tsx tableFields). Adds Holo + FirstEdition,
|
||||
// drops Foil.
|
||||
enum class PokemonSortColumn {
|
||||
Name,
|
||||
SetReleaseDate,
|
||||
Language,
|
||||
Condition,
|
||||
Amount,
|
||||
Holo,
|
||||
FirstEdition,
|
||||
Signed,
|
||||
Altered,
|
||||
Note,
|
||||
};
|
||||
|
||||
// Stable in-place sort. `ascending=false` runs the same comparator with
|
||||
// inverted sign, matching `byField(field, asc)` semantics.
|
||||
void sortMagicCards(std::vector<MagicCard>& cards, MagicSortColumn column,
|
||||
bool ascending);
|
||||
void sortPokemonCards(std::vector<PokemonCard>& cards, PokemonSortColumn column,
|
||||
bool ascending);
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
// CollectionService<TCard>: high-level CRUD over the per-game collection.
|
||||
// Header-only template that operates on the ICollectionRepository<T> port and
|
||||
// delegates image cleanup to an IImageStore on remove.
|
||||
//
|
||||
// This is the C++ equivalent of the Rust generic helpers in
|
||||
// `templates/card_service_templates.rs` (add_entry_to_collection,
|
||||
// update_entry_in_collection, get_entry_by_id, delete_entry_by_id, ...).
|
||||
//
|
||||
// Each TCard must expose `std::uint32_t id` and `std::vector<std::string> images`.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/ports/ICollectionRepository.hpp"
|
||||
#include "ccm/ports/IImageStore.hpp"
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
template <typename TCard>
|
||||
class CollectionService {
|
||||
public:
|
||||
using Map = std::map<std::uint32_t, TCard>;
|
||||
|
||||
CollectionService(ICollectionRepository<TCard>& repo, IImageStore& imageStore)
|
||||
: repo_(repo), imageStore_(imageStore) {}
|
||||
|
||||
Result<std::vector<TCard>> list(Game game) {
|
||||
auto loaded = repo_.load(game);
|
||||
if (!loaded) return Result<std::vector<TCard>>::err(loaded.error());
|
||||
Map map = std::move(loaded).value();
|
||||
std::vector<TCard> out;
|
||||
out.reserve(map.size());
|
||||
for (auto& [id, card] : map) {
|
||||
(void)id;
|
||||
out.push_back(std::move(card));
|
||||
}
|
||||
return Result<std::vector<TCard>>::ok(std::move(out));
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::uint32_t nextId(const Map& map) noexcept {
|
||||
if (map.empty()) return 0;
|
||||
return map.rbegin()->first + 1;
|
||||
}
|
||||
|
||||
// Add a card. The `id` field on the input is overwritten with the next
|
||||
// free id (matching the Rust HashMap-keyed behavior).
|
||||
Result<std::uint32_t> add(Game game, TCard card) {
|
||||
auto loaded = repo_.load(game);
|
||||
if (!loaded) return Result<std::uint32_t>::err(loaded.error());
|
||||
Map map = std::move(loaded).value();
|
||||
const std::uint32_t newId = nextId(map);
|
||||
card.id = newId;
|
||||
map.emplace(newId, std::move(card));
|
||||
auto saved = repo_.save(game, map);
|
||||
if (!saved) return Result<std::uint32_t>::err(saved.error());
|
||||
return Result<std::uint32_t>::ok(newId);
|
||||
}
|
||||
|
||||
// Update an existing entry, identified by `card.id`. If the id is not
|
||||
// present, an error is returned.
|
||||
Result<void> update(Game game, TCard card) {
|
||||
auto loaded = repo_.load(game);
|
||||
if (!loaded) return Result<void>::err(loaded.error());
|
||||
Map map = std::move(loaded).value();
|
||||
auto it = map.find(card.id);
|
||||
if (it == map.end()) {
|
||||
return Result<void>::err("Card with id " + std::to_string(card.id) +
|
||||
" not found in collection.");
|
||||
}
|
||||
it->second = std::move(card);
|
||||
return repo_.save(game, map);
|
||||
}
|
||||
|
||||
// Remove the card with the given id. Also deletes any associated images
|
||||
// via the IImageStore (best-effort - image removal failures are logged in
|
||||
// the error string but the card itself is still purged from the JSON).
|
||||
Result<void> remove(Game game, std::uint32_t id) {
|
||||
auto loaded = repo_.load(game);
|
||||
if (!loaded) return Result<void>::err(loaded.error());
|
||||
Map map = std::move(loaded).value();
|
||||
auto it = map.find(id);
|
||||
if (it == map.end()) {
|
||||
return Result<void>::err("Card with id " + std::to_string(id) + " not found.");
|
||||
}
|
||||
std::string imgErr;
|
||||
for (const auto& image : it->second.images) {
|
||||
auto rm = imageStore_.remove(game, image);
|
||||
if (!rm) {
|
||||
if (!imgErr.empty()) imgErr += "; ";
|
||||
imgErr += rm.error();
|
||||
}
|
||||
}
|
||||
map.erase(it);
|
||||
auto saved = repo_.save(game, map);
|
||||
if (!saved) return saved;
|
||||
if (!imgErr.empty()) {
|
||||
return Result<void>::err("Card removed but image cleanup had issues: " + imgErr);
|
||||
}
|
||||
return Result<void>::ok();
|
||||
}
|
||||
|
||||
// Look up a card by id without mutating storage.
|
||||
Result<std::optional<TCard>> findById(Game game, std::uint32_t id) {
|
||||
auto loaded = repo_.load(game);
|
||||
if (!loaded) return Result<std::optional<TCard>>::err(loaded.error());
|
||||
const auto& m = loaded.value();
|
||||
auto it = m.find(id);
|
||||
if (it == m.end()) return Result<std::optional<TCard>>::ok(std::nullopt);
|
||||
return Result<std::optional<TCard>>::ok(it->second);
|
||||
}
|
||||
|
||||
private:
|
||||
ICollectionRepository<TCard>& repo_;
|
||||
IImageStore& imageStore_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
// ConfigService: owns the live `Configuration`, persists it to `config.json`
|
||||
// next to the executable. Mirrors `util/config.rs` behavior - a missing file
|
||||
// is auto-created with sensible defaults on first launch.
|
||||
|
||||
#include "ccm/domain/Configuration.hpp"
|
||||
#include "ccm/ports/IFileSystem.hpp"
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class ConfigService {
|
||||
public:
|
||||
// `configFilePath` is the absolute path to `config.json`. The defaults
|
||||
// applied to a freshly created config use `defaultDataStorage` for the
|
||||
// dataStorage field.
|
||||
ConfigService(IFileSystem& fs,
|
||||
std::filesystem::path configFilePath,
|
||||
std::filesystem::path defaultDataStorage);
|
||||
|
||||
// Load (or create) the configuration. Must be called once at startup.
|
||||
Result<void> initialize();
|
||||
|
||||
[[nodiscard]] const Configuration& current() const noexcept { return current_; }
|
||||
|
||||
// Replace the live configuration and persist immediately.
|
||||
Result<void> store(Configuration cfg);
|
||||
|
||||
private:
|
||||
IFileSystem& fs_;
|
||||
std::filesystem::path path_;
|
||||
std::filesystem::path defaultDataStorage_;
|
||||
Configuration current_{};
|
||||
|
||||
[[nodiscard]] Configuration makeDefault() const;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
// ImageService: applies the established image filename rules and delegates the actual
|
||||
// disk operations to an IImageStore.
|
||||
//
|
||||
// Filename rule (mirrors Rust `magic/card_services.rs` and
|
||||
// `pokemon/card_services.rs`):
|
||||
// - new entry -> "{set}+{name}+{idx}.{ext}"
|
||||
// - existing -> "{id}+{set}+{name}+{idx}.{ext}"
|
||||
//
|
||||
// The "+name+set+name+" convention is preserved byte-for-byte so legacy
|
||||
// collections continue to display correctly when imported.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/ports/IImageStore.hpp"
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class ImageService {
|
||||
public:
|
||||
explicit ImageService(IImageStore& store);
|
||||
|
||||
// Compute the next image index from the previously stored image filenames
|
||||
// for a card. Implements the same logic as the Rust card_services modules.
|
||||
static std::uint8_t nextImageIndex(const std::vector<std::string>& existingImages);
|
||||
|
||||
// Build the target filename (without extension) for a new image attached
|
||||
// to a card. Pass the card's existing id (0 if not yet stored) and a
|
||||
// `newEntry` flag matching the established semantic.
|
||||
static std::string buildTargetName(bool newEntry,
|
||||
std::uint32_t cardId,
|
||||
const std::string& setName,
|
||||
const std::string& cardName,
|
||||
std::uint8_t index);
|
||||
|
||||
// Convenience: build target name + delegate copy to the IImageStore.
|
||||
Result<std::string> addImage(Game game,
|
||||
const std::filesystem::path& srcPath,
|
||||
bool newEntry,
|
||||
std::uint32_t cardId,
|
||||
const std::string& setName,
|
||||
const std::string& cardName,
|
||||
const std::vector<std::string>& existingImages);
|
||||
|
||||
Result<void> removeImage(Game game, const std::string& imageName);
|
||||
|
||||
// Ensure image filenames for a persisted card include the card id prefix.
|
||||
// This upgrades "create-mode" names (`set+name+idx.ext`) to
|
||||
// `id+set+name+idx.ext` to match the ccm2-compatible naming scheme.
|
||||
Result<std::vector<std::string>> normalizeNamesForPersistedCard(
|
||||
Game game,
|
||||
std::uint32_t cardId,
|
||||
const std::string& setName,
|
||||
const std::string& cardName,
|
||||
const std::vector<std::string>& imageNames);
|
||||
|
||||
[[nodiscard]] std::filesystem::path resolveImagePath(Game game,
|
||||
const std::string& imageName) const;
|
||||
|
||||
private:
|
||||
IImageStore& store_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
// SetService: high-level operations for fetching and caching set data.
|
||||
// Wraps an ISetRepository (cache) and dispatches to the correct ISetSource
|
||||
// based on the active IGameModule.
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/domain/Set.hpp"
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/ports/ISetRepository.hpp"
|
||||
#include "ccm/util/Result.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm {
|
||||
|
||||
class SetService {
|
||||
public:
|
||||
explicit SetService(ISetRepository& repo);
|
||||
|
||||
// Register a game module so this service can route fetch requests.
|
||||
// Pointer must remain valid for the lifetime of the SetService.
|
||||
void registerModule(IGameModule* module);
|
||||
|
||||
// Force a fresh fetch from the API for `game`, persist it via the
|
||||
// repository, and return the new list.
|
||||
Result<std::vector<Set>> updateSets(Game game);
|
||||
|
||||
// Cached read; returns an error if no local data exists yet.
|
||||
Result<std::vector<Set>> getSets(Game game);
|
||||
|
||||
private:
|
||||
ISetRepository& repo_;
|
||||
std::unordered_map<Game, IGameModule*> modules_;
|
||||
};
|
||||
|
||||
} // namespace ccm
|
||||
Reference in New Issue
Block a user