Add a Card Search tab and fix horizontal-card (Battle/Plane) preview orientation
Card Search browses the whole downloaded card universe (not just owned cards), reusing the Library tab's filter/group/sort/list/tiles UI; results show ownership and offer quick "own" and "add to deck" actions. Battles, Planes, and Phenomena store their Scryfall image portrait-shaped with the content itself rotated, so hover/click previews now detect this (a new `horizontal` flag threaded through ingest and every card query, with graceful fallback for a reference.db built before the column existed) and rotate the image back into place. Single-faced and non-DFC cases are verified fixed; a double-faced Battle's front face still needs another pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,11 @@ use crate::Catalog;
|
||||
|
||||
/// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value,
|
||||
/// colors, color_identity, type_line, image_uri, back_image_uri, produced_mana,
|
||||
/// produces_colorless, legalities, price_usd)` into its library parts. Shared
|
||||
/// by [`Catalog::list_library`] and the criteria search.
|
||||
/// produces_colorless, legalities, price_usd, horizontal)` into its library
|
||||
/// parts. Shared by [`Catalog::list_library`] and the criteria search. The
|
||||
/// caller's `SELECT` must put `horizontal` (or [`Catalog::horizontal_col`]'s
|
||||
/// `0` fallback) last, since it's not part of the base reference schema on
|
||||
/// every db (see `Catalog::has_horizontal_column`).
|
||||
pub(crate) fn row_to_library_parts(
|
||||
r: &rusqlite::Row,
|
||||
) -> rusqlite::Result<(String, i64, String, CardRef)> {
|
||||
@@ -34,6 +37,7 @@ pub(crate) fn row_to_library_parts(
|
||||
produces_colorless: r.get::<_, i64>(11)? != 0,
|
||||
legalities: Legalities(r.get::<_, i64>(12)?),
|
||||
price_usd: r.get::<_, Option<f64>>(13)?.unwrap_or_default(),
|
||||
horizontal: r.get::<_, i64>(14)? != 0,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -76,14 +80,15 @@ impl Catalog {
|
||||
let mut stmt = self.conn.prepare(&format!(
|
||||
"SELECT lc.oracle_id, lc.quantity_owned, lc.location,
|
||||
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back},
|
||||
oc.produced_mana, oc.produces_colorless, oc.legalities, {price}
|
||||
oc.produced_mana, oc.produces_colorless, oc.legalities, {price}, {horizontal}
|
||||
FROM library_cards lc
|
||||
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id
|
||||
WHERE lc.quantity_owned > 0
|
||||
ORDER BY oc.name COLLATE NOCASE",
|
||||
image = crate::db::image_subquery("image_uri"),
|
||||
back = crate::db::image_subquery("back_image_uri"),
|
||||
price = crate::db::price_subquery()
|
||||
price = crate::db::price_subquery(),
|
||||
horizontal = self.horizontal_col()
|
||||
))?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_library_parts)?
|
||||
@@ -161,6 +166,17 @@ impl Catalog {
|
||||
})
|
||||
}
|
||||
|
||||
/// Distinct type/subtype/keyword values across the *whole* reference db
|
||||
/// (every downloaded card, owned or not) — the Card Search tab's
|
||||
/// equivalent of [`Catalog::library_facets`].
|
||||
pub fn reference_facets(&self) -> Result<LibraryFacets> {
|
||||
Ok(LibraryFacets {
|
||||
types: self.distinct_facet("card_types", "type")?,
|
||||
subtypes: self.distinct_facet("card_subtypes", "subtype")?,
|
||||
keywords: self.distinct_facet("card_keywords", "keyword")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// `table`/`column` are always one of the three hardcoded call sites above
|
||||
/// (never user input), so interpolating them into the query is safe.
|
||||
fn distinct_owned_facet(&self, table: &str, column: &str) -> Result<Vec<String>> {
|
||||
@@ -174,6 +190,17 @@ impl Catalog {
|
||||
Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
/// Same as [`Catalog::distinct_owned_facet`] but unfiltered by ownership.
|
||||
fn distinct_facet(&self, table: &str, column: &str) -> Result<Vec<String>> {
|
||||
let mut stmt = self.conn.prepare(&format!(
|
||||
"SELECT DISTINCT x.{column}
|
||||
FROM {REF_DB}.{table} x
|
||||
ORDER BY x.{column} COLLATE NOCASE"
|
||||
))?;
|
||||
let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
|
||||
Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
/// Set the free-text storage location for a card (creates the library row if
|
||||
/// absent). Empty string clears it.
|
||||
pub fn set_location(&self, oracle_id: &str, location: &str) -> Result<()> {
|
||||
@@ -256,6 +283,18 @@ mod tests {
|
||||
assert!(facets.keywords.is_empty()); // Flying/Vigilance only on the unowned Serra Angel
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_facets_include_unowned_cards() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
// Nothing owned at all — reference_facets should still see everything
|
||||
// in the fixture, unlike library_facets.
|
||||
let facets = cat.reference_facets().unwrap();
|
||||
assert_eq!(facets.types, vec!["Artifact", "Creature", "Instant"]);
|
||||
assert!(facets.keywords.contains(&"Flying".to_string()));
|
||||
assert!(facets.keywords.contains(&"Vigilance".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_stats_aggregates_counts_and_colors() {
|
||||
let fx = test_support::make();
|
||||
|
||||
Reference in New Issue
Block a user