Card filter: show only Universes Beyond cards
The Universes Beyond toggle now cycles off → hide → only, backed by a new only_universes_beyond library filter. Choosing "only" counts as a real query, so Card Search runs it on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,9 @@ fn push_card_filters(
|
|||||||
if filter.exclude_universes_beyond {
|
if filter.exclude_universes_beyond {
|
||||||
sql.push_str(" AND oc.universes_beyond = 0");
|
sql.push_str(" AND oc.universes_beyond = 0");
|
||||||
}
|
}
|
||||||
|
if filter.only_universes_beyond {
|
||||||
|
sql.push_str(" AND oc.universes_beyond = 1");
|
||||||
|
}
|
||||||
if filter.exclude_digital_only {
|
if filter.exclude_digital_only {
|
||||||
sql.push_str(" AND oc.digital_only = 0");
|
sql.push_str(" AND oc.digital_only = 0");
|
||||||
}
|
}
|
||||||
@@ -387,7 +390,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn excluding_universes_beyond_hides_flagged_cards_only_when_asked() {
|
fn universes_beyond_filters_hide_or_keep_only_flagged_cards() {
|
||||||
let fx = owned_fixture();
|
let fx = owned_fixture();
|
||||||
// Stand in for what the ingest derives: a card that exists only as
|
// Stand in for what the ingest derives: a card that exists only as
|
||||||
// Universes Beyond.
|
// Universes Beyond.
|
||||||
@@ -410,6 +413,16 @@ mod tests {
|
|||||||
// Off unless asked for — this must not quietly change existing searches.
|
// Off unless asked for — this must not quietly change existing searches.
|
||||||
let all = names(&fx.catalog.search_library(&LibraryFilter::default()).unwrap());
|
let all = names(&fx.catalog.search_library(&LibraryFilter::default()).unwrap());
|
||||||
assert!(all.contains(&"Serra Angel".to_string()));
|
assert!(all.contains(&"Serra Angel".to_string()));
|
||||||
|
|
||||||
|
let f = LibraryFilter {
|
||||||
|
only_universes_beyond: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
names(&fx.catalog.search_library(&f).unwrap()),
|
||||||
|
vec!["Serra Angel"],
|
||||||
|
"only the flagged card is kept"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -421,6 +421,10 @@ pub struct LibraryFilter {
|
|||||||
/// existed, the same way the other reference-schema additions degrade.
|
/// existed, the same way the other reference-schema additions degrade.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub exclude_universes_beyond: bool,
|
pub exclude_universes_beyond: bool,
|
||||||
|
/// The opposite: only cards that exist only as Universes Beyond. Inert
|
||||||
|
/// against an older `reference.db` in the same way.
|
||||||
|
#[serde(default)]
|
||||||
|
pub only_universes_beyond: bool,
|
||||||
/// Hide cards that exist only on Arena/MTGO (Alchemy rebalances and the
|
/// Hide cards that exist only on Arena/MTGO (Alchemy rebalances and the
|
||||||
/// like) and so can never be in a physical collection.
|
/// like) and so can never be in a physical collection.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ export interface LibraryFilter {
|
|||||||
* Warhammer 40,000, Marvel, …). Ordinary cards reprinted into a UB set are
|
* Warhammer 40,000, Marvel, …). Ordinary cards reprinted into a UB set are
|
||||||
* kept. Needs card data downloaded after this shipped; inert otherwise. */
|
* kept. Needs card data downloaded after this shipped; inert otherwise. */
|
||||||
exclude_universes_beyond?: boolean;
|
exclude_universes_beyond?: boolean;
|
||||||
|
/** The opposite: only cards that exist only as Universes Beyond. */
|
||||||
|
only_universes_beyond?: boolean;
|
||||||
/** Hide Arena/MTGO-only cards (Alchemy rebalances) — they can't be owned on
|
/** Hide Arena/MTGO-only cards (Alchemy rebalances) — they can't be owned on
|
||||||
* paper. Needs card data downloaded after this shipped; inert otherwise. */
|
* paper. Needs card data downloaded after this shipped; inert otherwise. */
|
||||||
exclude_digital_only?: boolean;
|
exclude_digital_only?: boolean;
|
||||||
|
|||||||
@@ -2391,6 +2391,8 @@ const ICON_PACKAGE = icon(
|
|||||||
*/
|
*/
|
||||||
export type CardCriteria = {
|
export type CardCriteria = {
|
||||||
excludeUb: boolean;
|
excludeUb: boolean;
|
||||||
|
/** Never set together with `excludeUb`: the toggle cycles off → hide → only. */
|
||||||
|
onlyUb: boolean;
|
||||||
excludeDigital: boolean;
|
excludeDigital: boolean;
|
||||||
excludeFunny: boolean;
|
excludeFunny: boolean;
|
||||||
priceMin: string;
|
priceMin: string;
|
||||||
@@ -2410,6 +2412,7 @@ export type CardCriteria = {
|
|||||||
|
|
||||||
export const EMPTY_CRITERIA: CardCriteria = {
|
export const EMPTY_CRITERIA: CardCriteria = {
|
||||||
excludeUb: false,
|
excludeUb: false,
|
||||||
|
onlyUb: false,
|
||||||
excludeDigital: false,
|
excludeDigital: false,
|
||||||
excludeFunny: false,
|
excludeFunny: false,
|
||||||
priceMin: "",
|
priceMin: "",
|
||||||
@@ -2451,6 +2454,7 @@ const num = (s: string): number | null => (s.trim() === "" ? null : Number(s));
|
|||||||
export function applyCardCriteria(c: CardCriteria): Partial<LibraryFilter> {
|
export function applyCardCriteria(c: CardCriteria): Partial<LibraryFilter> {
|
||||||
return {
|
return {
|
||||||
exclude_universes_beyond: c.excludeUb,
|
exclude_universes_beyond: c.excludeUb,
|
||||||
|
only_universes_beyond: c.onlyUb,
|
||||||
exclude_digital_only: c.excludeDigital,
|
exclude_digital_only: c.excludeDigital,
|
||||||
exclude_funny: c.excludeFunny,
|
exclude_funny: c.excludeFunny,
|
||||||
price_min: num(c.priceMin),
|
price_min: num(c.priceMin),
|
||||||
@@ -2474,10 +2478,12 @@ export function applyCardCriteria(c: CardCriteria): Partial<LibraryFilter> {
|
|||||||
* The three "exclude" toggles deliberately don't count: they refine a result
|
* The three "exclude" toggles deliberately don't count: they refine a result
|
||||||
* set rather than select one, so ticking "hide Un-sets" on its own shouldn't
|
* set rather than select one, so ticking "hide Un-sets" on its own shouldn't
|
||||||
* make Card Search pull every card in the database. Choosing a format, a
|
* make Card Search pull every card in the database. Choosing a format, a
|
||||||
* power range, or "not owned" is a real query, and does.
|
* power range, "not owned" or "only Universes Beyond" is a real query, and
|
||||||
|
* does.
|
||||||
*/
|
*/
|
||||||
export function criteriaActive(c: CardCriteria): boolean {
|
export function criteriaActive(c: CardCriteria): boolean {
|
||||||
return (
|
return (
|
||||||
|
c.onlyUb ||
|
||||||
c.canBeCommander ||
|
c.canBeCommander ||
|
||||||
c.vanillaOnly ||
|
c.vanillaOnly ||
|
||||||
c.notOwned ||
|
c.notOwned ||
|
||||||
@@ -2611,13 +2617,29 @@ export function CardCriteriaFields({
|
|||||||
<div className="icon-toggles">
|
<div className="icon-toggles">
|
||||||
{/* Exclusions read as "no X" — tinted with the danger colour when
|
{/* Exclusions read as "no X" — tinted with the danger colour when
|
||||||
active, so removing things looks different from selecting them. */}
|
active, so removing things looks different from selecting them. */}
|
||||||
|
{/* Three states: off → hide → only → off. "Only" looks like the
|
||||||
|
other "only" toggles rather than an exclusion. */}
|
||||||
<IconToggle
|
<IconToggle
|
||||||
icon={ICON_CROSSOVER}
|
icon={ICON_CROSSOVER}
|
||||||
label="Hide Universes Beyond"
|
label={
|
||||||
hint="Cards that only exist as another property — Lord of the Rings, Warhammer 40,000, Marvel. Normal cards reprinted in those sets stay."
|
c.onlyUb
|
||||||
exclude
|
? "Only Universes Beyond (click to show all)"
|
||||||
on={c.excludeUb}
|
: c.excludeUb
|
||||||
onToggle={() => onChange({ excludeUb: !c.excludeUb })}
|
? "Hiding Universes Beyond (click for only them)"
|
||||||
|
: "Hide Universes Beyond"
|
||||||
|
}
|
||||||
|
hint="Cards that only exist as another property — Lord of the Rings, Warhammer 40,000, Marvel. Normal cards reprinted in those sets count as normal cards. Click again to show only these cards, and once more to show everything."
|
||||||
|
exclude={!c.onlyUb}
|
||||||
|
on={c.excludeUb || c.onlyUb}
|
||||||
|
onToggle={() =>
|
||||||
|
onChange(
|
||||||
|
c.onlyUb
|
||||||
|
? { excludeUb: false, onlyUb: false }
|
||||||
|
: c.excludeUb
|
||||||
|
? { excludeUb: false, onlyUb: true }
|
||||||
|
: { excludeUb: true, onlyUb: false },
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<IconToggle
|
<IconToggle
|
||||||
icon={ICON_SCREEN}
|
icon={ICON_SCREEN}
|
||||||
|
|||||||
Reference in New Issue
Block a user