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
This commit is contained in:
2026-10-23 22:56:59 -05:00
parent e4956d8231
commit d4d19dcdb4
2 changed files with 59 additions and 2 deletions

View File

@@ -98,6 +98,15 @@ const ICON_UPLOAD = icon(
<path d="M4 18v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1" />
</>,
);
// A box (as on the "Not owned" filter) with a plus: add to the library.
const ICON_LIBRARY_ADD = icon(
<>
<path d="M20 11V8l-8-4.5L4 8v8l8 4.5" />
<path d="M4 8l8 4.5L20 8" />
<path d="M12 12.5v8" />
<path d="M18 14v7M14.5 17.5h7" />
</>,
);
/**
* 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 ------------------------------------------------------------

View File

@@ -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={
<IconButton overlay {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
<span className="row" style={{ gap: 4 }}>
<IconButton overlay {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
{e.quantity_owned === 0 && (
<IconButton
overlay
icon={Icons.libraryAdd}
label={libraryAddLabel(e)}
onClick={() => addToLibrary([e])}
/>
)}
</span>
}
footer={e.is_commander ? <span className="tile-badge">★ commander</span> : undefined}
/>
@@ -960,7 +995,12 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
</td>
<td data-label="Owned">{e.quantity_owned}</td>
<td>
<IconButton {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
<span className="row" style={{ gap: 4 }}>
<IconButton {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
{e.quantity_owned === 0 && (
<IconButton icon={Icons.libraryAdd} label={libraryAddLabel(e)} onClick={() => addToLibrary([e])} />
)}
</span>
</td>
</tr>
))}
@@ -1264,6 +1304,13 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
<button className="danger" onClick={bulkRemoveFromDeck} disabled={bulkBusy}>
Remove from deck
</button>
<button
onClick={bulkAddToLibrary}
disabled={bulkBusy || !contents.some((e) => selectedCards.has(e.oracle_id) && shortBy(e) > 0)}
title="Add the copies these cards are missing to your library"
>
Add to library
</button>
</div>
<span className="spacer" />
<button onClick={() => setSelectedCards(new Set())} disabled={bulkBusy}>