Repository navigation
Decklist Panel
English · Español
The left panel of the deck screen (src/features/decklist/). It owns the text box where the user pastes a card list, the buttons that turn that list into cards (replace or append), the "Detect tokens" dialog, the Archidekt URL import, own-image uploads, and the resolve options (language, image policy, preview quality). Everything heavy (parsing, Scryfall, downloads) runs in Rust; the panel only drives it and paints state.
For the parser itself see Decklist Parsing; for the Scryfall side of resolving see Scryfall Integration.

| File | Responsibility |
|---|---|
src/features/decklist/decklist-panel.tsx |
DecklistPanel (layout, buttons, options, confirmations) and PiePanel (footer: running job or summary) |
src/features/decklist/editor-lista.tsx |
EditorLista: the text box with line numbers and red marks on lines Rust could not read |
src/features/decklist/acciones.ts |
Plain functions behind the buttons: resolver, cargarLista, reaplicarIdioma, anadirPropias, subirCartasPropias, subirReverso, reversoGenerico, elegirImagenes, cancelarTrabajo
|
src/features/decklist/fichas-dialog.tsx |
FichasDialog: "Detect tokens" review dialog |
src/features/decklist/ejemplo.ts |
EJEMPLO: the sample list behind the "Example" button |
src/features/decklist/__tests__/acciones.test.ts |
Vitest coverage of acciones.ts
|
src-tauri/src/commands/decklist.rs |
parse_decklist, deck_list_text, deck_list_save
|
src-tauri/src/commands/cards.rs |
resolve_decklist, job_cancel, deck_tokens_detect, import_archidekt
|
Decision: the button logic lives in acciones.ts, outside the components. Why: the context menu of a card and the drag-and-drop of files onto the preview reuse the same functions (src/features/preview/arrastrar-ficheros.ts imports anadirPropias, EXTENSIONES_IMAGEN from it).
DecklistPanel is a vertical ResizablePanelGroup (react-resizable-panels, layout persisted in localStorage under proxybuilder.panel-lista) with two panels:
-
lista: theEditorListatext box. Its height is user-adjustable through the handle. -
opciones: a fixed strip (line count, "Load list", "Add", "Add tokens automatically" checkbox, "Detect tokens") and, below it, a sharedScrollAreawith three collapsibleSeccions: "Import from URL", "Custom images", "Options".
Header buttons: "Sets" (opens the Set Browser via useUi.getState().abrir({ tipo: "sets" })), "Example" (setSinHistorial({ decklistText: EJEMPLO })), "Clear" (empties only the text box; the deck's cards are untouched).
The footer PiePanel shows either the running job (useTrabajo -> trabajo.fase, hechas/total, a Progress bar and a Cancel button, disabled until trabajo.jobId is known) or "N cards · P sheets" (useDeck card count and usePlan plan.sheets), plus a "N marked" count of cards with DeckCard.marca and the last action message (useUi.ultimaAccion, auto-cleared after 8 s).
EditorLista is a plain <textarea> that grows with its content inside the shared ScrollArea (so numbers and text scroll together and the project's scroll rules hold). Line height is a constant LINEA = 20 px shared by the gutter and the textarea.
- 250 ms after the last keystroke it calls
call("parse_decklist", { text }). Avigenteflag drops stale responses; a failure is ignored because the next keystroke re-parses. - The result (
ParsedDecklist:lines,cardCount,invalidCount) goes up throughonLeidato the panel, which usesleida.cardCountto enable the load buttons and shows "N cards in the list" / "N lines not understood" in red. - Lines with
kind === "invalid"get a red gutter number with a tooltip (MOTIVOS: one message perLineError:zeroQuantity,badQuantity,tooMany,emptyName) and a red band behind the text.
parse_decklist (commands/decklist.rs) is a synchronous, cheap #[tauri::command] that just calls proxy_core::decklist::parse. The text itself is deck state: useDeck -> deck.decklistText, written through setSinHistorial (a change that does not enter the undo history but is autosaved; see State Stores).
sequenceDiagram
participant UI as DecklistPanel
participant A as acciones.ts
participant R as Rust commands/cards.rs
participant S as resolve::resolve
UI->>A: cargarLista(texto, "reemplazar" | "anadir", t)
A->>R: parse_decklist(text)
A->>A: resolver(lista, t)
A->>R: lanzarTrabajo: resolve_decklist(list, options)
R-->>A: jobId (spawned task)
R->>S: resolve(api, EnLaApp, list, options, cancelado, progress)
S-->>A: event resolve://progress {jobId, progress}
S-->>A: event resolve://done {jobId, result | error}
A->>A: deck.aplicar / setCartas(insertar)
Step by step:
-
pulsarCargarcheckshayCartas && ui.pedirConfirmacionAlCargar; if both are true it opens theConfirmardialog ("Replace the deck's cards?" with a "Don't ask again" checkbox that flips thepedirConfirmacionAlCargarpreference). "Add" never asks. -
cargarListare-parses (parse_decklist) so it never relies on a staleleida. A list withcardCount === 0announces "The list has no cards" and stops. -
resolverrefuses to start ifuseTrabajo.getState().actualis set (it throws{ code: "Busy", params: {} }), setsuseTrabajo.empezar("resolver", ...), and launches the job withlanzarTrabajo("resolve://progress", "resolve://done", ...)fromsrc/lib/trabajos.ts. The options sent are{ lang: ui.lang, imagePolicy: ui.imagePolicy, addTokens: ui.addTokens }(ResolveOptionsinsrc-tauri/src/scryfall/resolve.rs). -
lanzarTrabajosubscribes to both events before invoking the command and buffers events that arrive before the job id is known. Why: a short job can finish beforeinvokereturns its id. - Progress events carry
ResolvePhase(lookup,language,tokens,images);FASESmaps each to a translated label ("Searching cards…", "Searching the language…", "Searching tokens…", "Downloading images…"). - On
resolve://done:error.code === "Cancelled"->resolverreturnsnullandcargarListaannounces "Cancelled: nothing was changed"; any other error is thrown (toast viamensaje(e)); otherwise theResolveResult(cards: DeckCard[],unresolved: Unresolved[]) is applied. - Apply: replace ->
deck.aplicar(() => ({ cards: r.cards }))and clear the selection (undoable with Ctrl+Z); append ->deck.setCartas((c) => insertar(c, r.cards))(src/lib/deck-ops.ts, inserts at the end). -
avisarNoResueltasbuilds the warning "Not found: A, B, C, D, E (+N)" (first five names, then a count) fromr.unresolved; the panel'sintentarwrapper shows any returned string astoast.warning. - The
finallyinresolveralways callsuseTrabajo.terminar().
Rust side (resolve_decklist in src-tauri/src/commands/cards.rs): allocates a job (state.jobs.alta() -> (job_id, cancelado)), spawns an async task that holds the busy marker (estado.trabajo()), runs resolve::resolve with EnLaApp (an implementation of the Guardar trait that writes images into the blob store and records card data in the Scryfall cache), emits the two events and deregisters the job (jobs.baja). EnLaApp also provides the English-name lookup for non-English lines (fuentes::nombres_ingleses) and the Cockatrice token catalog fallbacks (externa_por_nombre, externas_creadas_por; see External Sources).
Section mapping inside the resolver: a Sideboard section arrives as MarcaCarta::Banquillo and a Maybeboard section as MarcaCarta::Opcional (src-tauri/src/scryfall/resolve.rs, Section::Sideboard => Some(MarcaCarta::Banquillo)). Marked cards are printed but do not count for deck size, copies or statistics (see Deck Model and Statistics).
Cancel: cancelarTrabajo calls job_cancel { jobId } (commands/cards.rs, returns bool), or export_cancel when the running job is an export. Cancelling leaves the deck untouched.
| Control | Store field (useUi) |
Notes |
|---|---|---|
Card language (IDIOMAS, built from IDIOMAS_SCRYFALL of src/lib/idiomas.ts: en, es, fr, de, it, pt, ja, ko, ru, zhs, zht, each shown by its own name, untranslated) |
lang |
Sent as ResolveOptions.lang. If the language does not exist for a card, the resolver falls back to English and marks meta.langFallback (badge "↩ EN"). |
| Images |
imagePolicy (ImagePolicy: withImage, bestAvailable, highresOnly) |
Which printing is chosen when the list does not give a set. |
| Preview quality |
calidadVista (244, 488, 672, 745 px) |
Screen only; the PDF always uses export resolution. |
| Ask before replacing | pedirConfirmacionAlCargar |
Default on. |
| Add tokens automatically | addTokens |
Default off. Tokens come from the English printing's all_parts. |
"Reapply language" -> reaplicarIdioma(t):
-
listaParaReaplicar(deck.cards)(src/lib/cartas.ts) builds a syntheticParsedDecklistwith one line per copy of every card that is not locked (c.locked), issource.kind === "scryfall"and has a name, using its set and collector number, and returns the parallel list of card ids. -
resolver(lista, t, { addTokens: false })runs the normal job (tokens off so the deck does not grow). -
aplicarReaplicado(cartas, ids, r.cards, noResueltas)pairs new cards with old ones in order, skipping lines whoselineNois inunresolved; the new card takes the oldid,editsandlocked, and keeps a custom back if the old card had one. - Result: "Language reapplied to N cards".
If nothing qualifies: "There are no unlocked Scryfall cards" and Rust is not called.
Opened by useUi.abrir({ tipo: "fichas" }) ("Detect tokens" button, disabled when busy or the deck is empty). It finds tokens that the cards already in the deck create and that the deck does not yet have, including own cards linked to Scryfall (see Card Identification and OCR).
- On open it calls
deck_tokens_detect { cards }with the current deck cards. ReturnsFichaPropuesta[]({ carta: CardSummary, creadaPor: string }). - Rust (
deck_tokens_detectincommands/cards.rs):scryfall::fichas_auto::faltan_en_el_deckcollects Scryfall ids from the deck, reads each card from the local cache (cache::leer) and follows the English printings'all_partsto find tokens; it excludes tokens already present by id, by the same token from another collection, or by name (also hand-made own cards). Found tokens are written to the Scryfall cache. Then it adds tokens that Scryfall lacks from the Cockatrice catalog (fuentes::cockatrice::catalogo,creadas_por(name)) unless a card with that name is already in the deck or in the proposals. Network errors from the Cockatrice catalog are swallowed (if let Ok(cat)), so Scryfall results still arrive. - The dialog shows the proposals in a
RejillaCartaswith all of them pre-selected, plus a list "{token}: created by {card}". - "Add N tokens" calls
traerCarta(p.carta)for each selected proposal (see Add Card and Variants) and appends them withinsertar(cs, nuevas). Announces "N tokens added".
FichasDialog passes cambiosSinGuardar={false} and refuses to close while adding (anadiendo).
The "Import from URL" section calls import_archidekt { url, includeSideboard } (commands/cards.rs). It extracts the deck id with archidekt::id_de_url, GETs https://archidekt.com/api/decks/{id}/ with a ProxyBuilder/<version> user agent, maps 404/403 to NotFound and other non-success statuses to Network, then archidekt::desde_json(text, include_sideboard) returns ArchidektImport { text, list }. The panel puts r.text into the text box (setSinHistorial) and announces "List imported from Archidekt (N cards)"; it does not load the cards: the user still presses "Load list". A bad URL is InvalidInput with motivo = "bad_url".
Gotcha: this is the one place where Rust talks to a host directly with reqwest::Client instead of going through fuentes::Externo (see External Sources). The panel text states that Moxfield cannot be imported (copy and paste its list instead).
- "Upload cards":
elegirImagenesopens the system dialog throughabrirParaImportar(src/lib/dialogos.ts, remembers the last import folder), filters byEXTENSIONES_IMAGEN(png,jpg,jpeg,webp), copies the files into the blob store withblob_import_files { paths }(returnsFaceImage[];commands/library.rs) and creates one custom card per image withcartaPropia(img, nombreDeFichero(ruta)). The card name is derived from the file name bynombreDeFichero(strips folder, extension, leading numbering like0001 -,T003 -,12., and trailing(1)/~2). -
anadirPropias(rutas, t, en?)is the same for paths that did not come from the dialog (drag and drop), remembering the folder withrecordar("importar", ...)and inserting at positionen. - "Upload back": the first image becomes
deck.defaultBack; "Back to generic back" restores the app's generated back (blob_generic_back). - "Link with a card" opens the batch linking dialog (
tipo: "vincularLote"); with more than one card selected it limits itself to the selection by default (soloSeleccion). Details in Add Card and Variants and Card Identification and OCR.
Images always go to the blob store (app_data_dir/blobs/), never to the cache; see Storage and Blobs.
"Clear thumbnail cache" first calls cache_stats and shows a Confirmar with size and file count (megas() formats bytes); on confirm cache_clear deletes app_cache_dir/renders/ (commands/library.rs, vaciar). It can never touch the blob store, so deck images survive.
The list as text is produced by deck_list_text { deck, format } and saved by deck_list_save { deck, format, path } (commands/decklist.rs; FormatoLista, proxy_core::decklist::exportar). They are driven by the three-dot menu of the export button, see Export. deck_list_save requires an absolute path ending in .txt (else InvalidInput) and writes atomically (escribir_atomico).
All handlers are wrapped by intentar, which toasts mensaje(e) (see IPC and Error Handling). Codes the panel can surface: Busy (a job is already running), Network / RateLimited (Scryfall, Archidekt), NotFound (private or unknown Archidekt deck), InvalidInput (bad URL), Cancelled (handled silently as a cancel).
-
src/features/decklist/__tests__/acciones.test.ts:resolver(options passed, phases, Busy, cancel, error, Rust failing to launch),cargarLista(replace, append, unresolved warning of up to five names, cancel),reaplicarIdioma,cancelarTrabajo(per job type), own images and backs. - Rust:
src-tauri/src/commands/cards.rstests (queries, thumbnail registry,traer_cartacases) and the resolver tests insrc-tauri/src/scryfall/resolve.rs.
- New button that adds cards: write the function in
acciones.ts(so menus and drag-and-drop can reuse it) and calluseDeck.getState().setCartas(...). - New resolve option: add the field to
ResolveOptionsin Rust, runpnpm bindings, add the preference tosrc/stores/ui-store.tsand pass it inresolver. - Another long job: reuse
lanzarTrabajo(listen before launching) anduseTrabajo.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia