Skip to content

Panel de la lista del mazo

Pablo Martínez López edited this page Oct 21, 2026 · 2 revisions

Panel de la lista del mazo

English · Español

El panel izquierdo de la pantalla del mazo (src/features/decklist/). Contiene el cuadro de texto donde el usuario pega una lista de cartas, los botones que convierten esa lista en cartas (reemplazando o añadiendo), el diálogo «Detectar fichas», la importación por URL de Archidekt, la subida de imágenes propias y las opciones de resolución (idioma, política de imágenes, calidad de la vista previa). Todo lo pesado (parseo, Scryfall, descargas) se ejecuta en Rust; el panel solo lo dirige y pinta el estado.

Para el parser en sí véase Parseo de la lista del mazo; para la parte de Scryfall al resolver, véase Integración con Scryfall.

Vista principal

Ficheros

Fichero Responsabilidad
src/features/decklist/decklist-panel.tsx DecklistPanel (maquetación, botones, opciones, confirmaciones) y PiePanel (pie: trabajo en curso o resumen)
src/features/decklist/editor-lista.tsx EditorLista: el cuadro de texto con números de línea y marcas rojas en las líneas que Rust no pudo leer
src/features/decklist/acciones.ts Funciones simples detrás de los botones: resolver, cargarLista, reaplicarIdioma, anadirPropias, subirCartasPropias, subirReverso, reversoGenerico, elegirImagenes, cancelarTrabajo
src/features/decklist/fichas-dialog.tsx FichasDialog: diálogo de revisión de «Detectar fichas»
src/features/decklist/ejemplo.ts EJEMPLO: la lista de muestra detrás del botón «Ejemplo»
src/features/decklist/__tests__/acciones.test.ts Cobertura de Vitest de 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

Decisión: la lógica de los botones vive en acciones.ts, fuera de los componentes. Motivo: el menú contextual de una carta y el arrastre de ficheros sobre la vista previa reutilizan las mismas funciones (src/features/preview/arrastrar-ficheros.ts importa de ahí anadirPropias y EXTENSIONES_IMAGEN).

Maquetación

DecklistPanel es un ResizablePanelGroup vertical (react-resizable-panels, con la disposición guardada en localStorage bajo proxybuilder.panel-lista) con dos paneles:

  1. lista: el cuadro de texto EditorLista. El usuario puede ajustar su altura con el tirador.
  2. opciones: una franja fija (contador de líneas, «Cargar lista», «Añadir», casilla «Añadir tokens automáticamente», «Detectar fichas») y, debajo, un ScrollArea compartido con tres Secciones plegables: «Importar desde URL», «Imágenes propias», «Opciones».

Botones de la cabecera: «Sets» (abre el Explorador de sets con useUi.getState().abrir({ tipo: "sets" })), «Ejemplo» (setSinHistorial({ decklistText: EJEMPLO })), «Limpiar» (vacía solo el cuadro de texto; las cartas del mazo no se tocan).

El pie PiePanel muestra o bien el trabajo en curso (useTrabajo -> trabajo.fase, hechas/total, una barra Progress y un botón Cancelar, deshabilitado hasta que se conoce trabajo.jobId) o bien «N cartas · P hojas» (recuento de cartas de useDeck y plan.sheets de usePlan), además de un recuento «N marcadas» de las cartas con DeckCard.marca y el último mensaje de acción (useUi.ultimaAccion, que se borra solo a los 8 s).

El cuadro de texto y el parseo en vivo

EditorLista es un <textarea> simple que crece con su contenido dentro del ScrollArea compartido (así los números y el texto se desplazan juntos y se cumplen las reglas de scroll del proyecto). La altura de línea es una constante LINEA = 20 px compartida por el margen de números y el textarea.

  • 250 ms después de la última pulsación llama a call("parse_decklist", { text }). Una bandera vigente descarta las respuestas obsoletas; un fallo se ignora porque la siguiente pulsación vuelve a parsear.
  • El resultado (ParsedDecklist: lines, cardCount, invalidCount) sube por onLeida hasta el panel, que usa leida.cardCount para habilitar los botones de carga y muestra «N cartas en la lista» / «N líneas sin entender» en rojo.
  • Las líneas con kind === "invalid" tienen el número del margen en rojo con un tooltip (MOTIVOS: un mensaje por cada LineError: zeroQuantity, badQuantity, tooMany, emptyName) y una banda roja detrás del texto.

parse_decklist (commands/decklist.rs) es un #[tauri::command] síncrono y barato que se limita a llamar a proxy_core::decklist::parse. El texto en sí es estado del mazo: useDeck -> deck.decklistText, que se escribe con setSinHistorial (un cambio que no entra en el historial de deshacer pero sí se autoguarda; véase Stores de estado).

Flujo: «Cargar lista» y «Añadir»

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)
Loading

Paso a paso:

  1. pulsarCargar comprueba hayCartas && ui.pedirConfirmacionAlCargar; si ambas son verdaderas abre el diálogo Confirmar («¿Reemplazar las cartas del deck?» con una casilla «No volver a preguntar» que cambia la preferencia pedirConfirmacionAlCargar). «Añadir» nunca pregunta.
  2. cargarLista vuelve a parsear (parse_decklist) para no depender nunca de un leida obsoleto. Una lista con cardCount === 0 anuncia «La lista no tiene cartas» y se detiene.
  3. resolver se niega a empezar si useTrabajo.getState().actual está definido (lanza { code: "Busy", params: {} }), llama a useTrabajo.empezar("resolver", ...) y lanza el trabajo con lanzarTrabajo("resolve://progress", "resolve://done", ...) de src/lib/trabajos.ts. Las opciones enviadas son { lang: ui.lang, imagePolicy: ui.imagePolicy, addTokens: ui.addTokens } (ResolveOptions en src-tauri/src/scryfall/resolve.rs).
  4. lanzarTrabajo se suscribe a ambos eventos antes de invocar el comando y guarda en un búfer los eventos que llegan antes de conocerse el id del trabajo. Motivo: un trabajo corto puede terminar antes de que invoke devuelva su id.
  5. Los eventos de progreso llevan una ResolvePhase (lookup, language, tokens, images); FASES asigna a cada una una etiqueta traducida («Buscando cartas…», «Buscando el idioma…», «Buscando tokens…», «Descargando imágenes…»).
  6. En resolve://done: con error.code === "Cancelled", resolver devuelve null y cargarLista anuncia «Cancelado: no se ha cambiado nada»; cualquier otro error se lanza (toast mediante mensaje(e)); en caso contrario se aplica el ResolveResult (cards: DeckCard[], unresolved: Unresolved[]).
  7. Aplicar: reemplazar -> deck.aplicar(() => ({ cards: r.cards })) y se limpia la selección (se puede deshacer con Ctrl+Z); añadir -> deck.setCartas((c) => insertar(c, r.cards)) (src/lib/deck-ops.ts, inserta al final).
  8. avisarNoResueltas construye el aviso «Sin encontrar: A, B, C, D, E (+N)» (los cinco primeros nombres y luego un recuento) a partir de r.unresolved; el envoltorio intentar del panel muestra como toast.warning cualquier cadena devuelta.
  9. El finally de resolver siempre llama a useTrabajo.terminar().

Lado de Rust (resolve_decklist en src-tauri/src/commands/cards.rs): reserva un trabajo (state.jobs.alta() -> (job_id, cancelado)), lanza una tarea asíncrona que mantiene la marca de ocupado (estado.trabajo()), ejecuta resolve::resolve con EnLaApp (una implementación del trait Guardar que escribe las imágenes en el almacén de blobs y registra los datos de las cartas en la caché de Scryfall), emite los dos eventos y da de baja el trabajo (jobs.baja). EnLaApp también ofrece la búsqueda de nombres en inglés para las líneas que no están en inglés (fuentes::nombres_ingleses) y los respaldos del catálogo de fichas de Cockatrice (externa_por_nombre, externas_creadas_por; véase Fuentes externas).

Correspondencia de secciones dentro del resolutor: una sección Sideboard llega como MarcaCarta::Banquillo y una sección Maybeboard como MarcaCarta::Opcional (src-tauri/src/scryfall/resolve.rs, Section::Sideboard => Some(MarcaCarta::Banquillo)). Las cartas marcadas se imprimen pero no cuentan para el tamaño del mazo, las copias ni las estadísticas (véanse Modelo del mazo y Estadísticas).

Cancelar: cancelarTrabajo llama a job_cancel { jobId } (commands/cards.rs, devuelve bool), o a export_cancel cuando el trabajo en curso es una exportación. Cancelar deja el mazo intacto.

Sección de opciones

Control Campo del store (useUi) Notas
Idioma de las cartas (IDIOMAS, hecho con IDIOMAS_SCRYFALL de src/lib/idiomas.ts: en, es, fr, de, it, pt, ja, ko, ru, zhs, zht, cada uno con su nombre propio, sin traducir) lang Se envía como ResolveOptions.lang. Si el idioma no existe para una carta, el resolutor recurre al inglés y marca meta.langFallback (insignia «↩ EN»).
Imágenes imagePolicy (ImagePolicy: withImage, bestAvailable, highresOnly) Qué impresión se elige cuando la lista no indica el set.
Calidad de la vista previa calidadVista (244, 488, 672, 745 px) Solo en pantalla; el PDF siempre usa la resolución de exportación.
Preguntar antes de reemplazar las cartas pedirConfirmacionAlCargar Activado por defecto.
Añadir tokens automáticamente addTokens Desactivado por defecto. Las fichas salen del all_parts de la impresión en inglés.

Reaplicar idioma

«Reaplicar idioma» -> reaplicarIdioma(t):

  1. listaParaReaplicar(deck.cards) (src/lib/cartas.ts) construye una ParsedDecklist sintética con una línea por cada copia de cada carta que no esté fijada (c.locked), tenga source.kind === "scryfall" y tenga nombre, usando su set y su número de coleccionista, y devuelve la lista paralela de ids de carta.
  2. resolver(lista, t, { addTokens: false }) ejecuta el trabajo normal (con las fichas desactivadas para que el mazo no crezca).
  3. aplicarReaplicado(cartas, ids, r.cards, noResueltas) empareja por orden las cartas nuevas con las antiguas, saltándose las líneas cuyo lineNo está en unresolved; la carta nueva toma el id, los edits y el locked de la antigua, y conserva un reverso propio si la antigua lo tenía.
  4. Resultado: «Idioma reaplicado a N cartas».

Si no hay ninguna que cumpla: «No hay cartas de Scryfall sin fijar» y no se llama a Rust.

Detectar fichas (FichasDialog)

Se abre con useUi.abrir({ tipo: "fichas" }) (botón «Detectar fichas», deshabilitado si hay un trabajo en curso o el mazo está vacío). Encuentra las fichas que crean las cartas que ya están en el mazo y que el mazo aún no tiene, incluidas las cartas propias vinculadas a Scryfall (véase Identificación de cartas y OCR).

  1. Al abrirse llama a deck_tokens_detect { cards } con las cartas actuales del mazo. Devuelve FichaPropuesta[] ({ carta: CardSummary, creadaPor: string }).
  2. Rust (deck_tokens_detect en commands/cards.rs): scryfall::fichas_auto::faltan_en_el_deck recoge los ids de Scryfall del mazo, lee cada carta de la caché local (cache::leer) y sigue el all_parts de las impresiones en inglés para encontrar fichas; excluye las fichas ya presentes por id, por la misma ficha de otra colección o por nombre (también las cartas propias hechas a mano). Las fichas encontradas se escriben en la caché de Scryfall. Después añade las fichas que Scryfall no tiene a partir del catálogo de Cockatrice (fuentes::cockatrice::catalogo, creadas_por(name)), salvo que ya haya una carta con ese nombre en el mazo o en las propuestas. Los errores de red del catálogo de Cockatrice se tragan (if let Ok(cat)), de modo que los resultados de Scryfall llegan igualmente.
  3. El diálogo muestra las propuestas en una RejillaCartas con todas preseleccionadas, más una lista «{ficha}: creada por {carta}».
  4. «Añadir N fichas» llama a traerCarta(p.carta) por cada propuesta seleccionada (véase Añadir carta y variantes) y las añade con insertar(cs, nuevas). Anuncia «N fichas añadidas».

FichasDialog pasa cambiosSinGuardar={false} y se niega a cerrarse mientras añade (anadiendo).

Importar desde Archidekt

La sección «Importar desde URL» llama a import_archidekt { url, includeSideboard } (commands/cards.rs). Extrae el id del mazo con archidekt::id_de_url, hace un GET a https://archidekt.com/api/decks/{id}/ con un user agent ProxyBuilder/<version>, asigna 404/403 a NotFound y los demás estados no exitosos a Network, y luego archidekt::desde_json(text, include_sideboard) devuelve ArchidektImport { text, list }. El panel pone r.text en el cuadro de texto (setSinHistorial) y anuncia «Lista importada de Archidekt (N cartas)»; no carga las cartas: el usuario aún tiene que pulsar «Cargar lista». Una URL incorrecta es InvalidInput con motivo = "bad_url".

Trampa conocida: este es el único sitio donde Rust habla con un host directamente con reqwest::Client en lugar de pasar por fuentes::Externo (véase Fuentes externas). El texto del panel indica que Moxfield no se puede importar (hay que copiar y pegar su lista).

Imágenes propias

  • «Subir cartas»: elegirImagenes abre el diálogo del sistema mediante abrirParaImportar (src/lib/dialogos.ts, que recuerda la última carpeta de importación), filtra por EXTENSIONES_IMAGEN (png, jpg, jpeg, webp), copia los ficheros al almacén de blobs con blob_import_files { paths } (devuelve FaceImage[]; commands/library.rs) y crea una carta propia por imagen con cartaPropia(img, nombreDeFichero(ruta)). El nombre de la carta se deriva del nombre del fichero con nombreDeFichero (quita la carpeta, la extensión, la numeración inicial como 0001 - , T003 - , 12. , y los (1) / ~2 finales).
  • anadirPropias(rutas, t, en?) hace lo mismo con rutas que no vienen del diálogo (arrastrar y soltar), recordando la carpeta con recordar("importar", ...) e insertando en la posición en.
  • «Subir reverso»: la primera imagen pasa a ser deck.defaultBack; «Volver al reverso genérico» restablece el reverso generado por la app (blob_generic_back).
  • «Vincular con una carta» abre el diálogo de vinculación en bloque (tipo: "vincularLote"); con más de una carta seleccionada se limita por defecto a la selección (soloSeleccion). Detalles en Añadir carta y variantes y Identificación de cartas y OCR.

Las imágenes siempre van al almacén de blobs (app_data_dir/blobs/), nunca a la caché; véase Almacenamiento y blobs.

Limpieza de la caché de miniaturas

«Limpiar caché de miniaturas» llama primero a cache_stats y muestra un Confirmar con el tamaño y el número de ficheros (megas() da formato a los bytes); al confirmar, cache_clear borra app_cache_dir/renders/ (commands/library.rs, vaciar). Nunca puede tocar el almacén de blobs, así que las imágenes de los mazos sobreviven.

Exportar la lista

La lista como texto la produce deck_list_text { deck, format } y la guarda deck_list_save { deck, format, path } (commands/decklist.rs; FormatoLista, proxy_core::decklist::exportar). Se manejan desde el menú de tres puntos del botón de exportar, véase Exportación. deck_list_save exige una ruta absoluta que acabe en .txt (si no, InvalidInput) y escribe de forma atómica (escribir_atomico).

Gestión de errores

Todos los manejadores están envueltos por intentar, que muestra mensaje(e) en un toast (véase IPC y manejo de errores). Códigos que el panel puede mostrar: Busy (ya hay un trabajo en curso), Network / RateLimited (Scryfall, Archidekt), NotFound (mazo de Archidekt privado o desconocido), InvalidInput (URL incorrecta), Cancelled (se trata en silencio como una cancelación).

Tests

  • src/features/decklist/__tests__/acciones.test.ts: resolver (opciones pasadas, fases, Busy, cancelación, error, fallo de Rust al lanzar), cargarLista (reemplazar, añadir, aviso de no resueltas de hasta cinco nombres, cancelación), reaplicarIdioma, cancelarTrabajo (por tipo de trabajo), imágenes propias y reversos.
  • Rust: los tests de src-tauri/src/commands/cards.rs (consultas, registro de miniaturas, casos de traer_carta) y los tests del resolutor en src-tauri/src/scryfall/resolve.rs.

Por dónde empezar para ampliarlo

  • Un botón nuevo que añade cartas: escribe la función en acciones.ts (para que los menús y el arrastre de ficheros puedan reutilizarla) y llama a useDeck.getState().setCartas(...).
  • Una opción de resolución nueva: añade el campo a ResolveOptions en Rust, ejecuta pnpm bindings, añade la preferencia a src/stores/ui-store.ts y pásala en resolver.
  • Otro trabajo largo: reutiliza lanzarTrabajo (escuchar antes de lanzar) y useTrabajo.

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally