From d4d19dcdb43c73005b53f1cbea446c09216e6ae8 Mon Sep 17 00:00:00 2001 From: Garrett Mills Date: Fri, 23 Oct 2026 22:56:59 -0500 Subject: [PATCH] Add not-owned deck cards to the library from the deck view Cards with no owned copies get an "Add to library" button (tile corner and list row) that records the deck's copy count as owned, and the selection bar gains a bulk "Add to library" that adds just the missing copies. Closes #10 --- src/components.tsx | 10 +++++++++ src/views/DeckView.tsx | 51 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/components.tsx b/src/components.tsx index 054621d..1ba0f14 100644 --- a/src/components.tsx +++ b/src/components.tsx @@ -98,6 +98,15 @@ const ICON_UPLOAD = icon( , ); +// A box (as on the "Not owned" filter) with a plus: add to the library. +const ICON_LIBRARY_ADD = icon( + <> + + + + + , +); /** * A small round icon-only button, used consistently for lightweight actions @@ -146,6 +155,7 @@ export const Icons = { cards: ICON_CARDS, download: ICON_DOWNLOAD, upload: ICON_UPLOAD, + libraryAdd: ICON_LIBRARY_ADD, }; // --- Dialogs ------------------------------------------------------------ diff --git a/src/views/DeckView.tsx b/src/views/DeckView.tsx index e4a107a..7096425 100644 --- a/src/views/DeckView.tsx +++ b/src/views/DeckView.tsx @@ -55,6 +55,14 @@ function useMediaQuery(query: string) { } /** Tooltip/label + icon for the per-card mainboard/maybeboard toggle. */ +/** Copies of a deck card you'd need to acquire to cover the deck. */ +const shortBy = (e: DeckEntry) => Math.max(0, e.quantity - e.quantity_owned); + +function libraryAddLabel(e: DeckEntry): string { + const n = shortBy(e); + return n === 1 ? "Add to library" : `Add ${n} copies to library`; +} + function boardToggleProps(e: DeckEntry) { return e.is_maybeboard ? { icon: Icons.arrowLeftCircle, label: "Move to deck" } @@ -763,6 +771,23 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) { } } + /** Record cards as owned: as many copies as the deck uses beyond those + * already owned, so an acquired card moves to "In your library". */ + async function addToLibrary(entries: DeckEntry[]) { + await Promise.all(entries.filter((e) => shortBy(e) > 0).map((e) => api.addOwned(e.oracle_id, shortBy(e)))); + await loadContents(); + } + + async function bulkAddToLibrary() { + setBulkBusy(true); + try { + await addToLibrary(contents.filter((e) => selectedCards.has(e.oracle_id))); + setSelectedCards(new Set()); + } finally { + setBulkBusy(false); + } + } + async function confirmDelete() { if (dialog?.kind !== "delete") return; await api.deleteDeck(dialog.deck.id); @@ -904,7 +929,17 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) { /> } cornerBottomLeft={ - setMaybeboard(e, !e.is_maybeboard)} /> + + setMaybeboard(e, !e.is_maybeboard)} /> + {e.quantity_owned === 0 && ( + addToLibrary([e])} + /> + )} + } footer={e.is_commander ? ★ commander : undefined} /> @@ -960,7 +995,12 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) { {e.quantity_owned} - setMaybeboard(e, !e.is_maybeboard)} /> + + setMaybeboard(e, !e.is_maybeboard)} /> + {e.quantity_owned === 0 && ( + addToLibrary([e])} /> + )} + ))} @@ -1264,6 +1304,13 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) { +