Repository navigation
Rust App Layer
English · Español
The app crate (proxy-builder, library proxy_builder_lib, in src-tauri/) wires Tauri to the pure proxy-core crate: it builds the window and plugins, owns AppState (database, blob store, HTTP clients, jobs), serves the pbimg:// image protocol, and registers every command. This page covers the boot sequence, shared state, the protocol and render cache, Tauri configuration (window, CSP, capabilities, plugins) and a module map of the crate.
| File | Role |
|---|---|
src-tauri/src/main.rs |
Six lines: fn main() { proxy_builder_lib::run() }. On Windows release builds #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] suppresses the console. |
src-tauri/src/lib.rs |
pub fn run() (marked #[cfg_attr(mobile, tauri::mobile_entry_point)]): builder, plugins, protocol, setup, generate_handler!. Also declares all modules and #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]. |
src-tauri/build.rs |
tauri_build::build(). |
src-tauri/Cargo.toml |
Library crate types staticlib, cdylib, rlib; the lib name has the _lib suffix to avoid a Windows name clash with the binary. Workspace members: the app and crates/proxy-core. Dev profile tweaks (see below). |
lib.rs is intentionally small: it only boots and registers. If it grows, move code into a module under commands/ or elsewhere.
sequenceDiagram
participant OS
participant RUN as lib.rs run()
participant TAU as Tauri runtime
participant ST as setup closure
participant WV as WebView (main window, hidden)
participant FE as main.tsx
OS->>RUN: start proxy_builder_lib::run
RUN->>TAU: Builder: plugins opener, dialog, updater, window-state
RUN->>TAU: manage(Actualizador)
RUN->>TAU: on_page_load(show window when Finished)
RUN->>TAU: register_asynchronous_uri_scheme_protocol("pbimg", protocol::manejador)
TAU->>ST: setup(app)
ST->>ST: Paths { data: app_data_dir, cache: app_cache_dir }
ST->>ST: registro::iniciar(app_log_dir)
ST->>ST: AppState::open(paths): dirs, library.db + migrations, rellenar_refs, BlobStore, Recursos, Scryfall, Externo
ST->>TAU: manage(AppState)
ST->>ST: spawn task: original::completar (missing Original frames)
ST->>ST: spawn thread: after 3 s show window if still hidden
TAU->>WV: create window (visible false, background 1d1b19)
WV->>FE: load index.html and JS
FE->>FE: prepararTemasInstalados, ponerTemaAntesDeMontar
FE->>WV: render React tree
FE->>TAU: getCurrentWindow().show()
TAU-->>WV: window shown (dark background, theme already applied)
Note over RUN,TAU: Rust also shows it on PageLoadEvent::Finished
Details and reasons:
-
The window starts hidden and dark.
tauri.conf.jsonsets"visible": falseand"backgroundColor": "#1d1b19". This avoids a white flash while the WebView loads. -
The window-state plugin must not make it visible.
tauri_plugin_window_state::Builderis configured withStateFlags::all() & !StateFlags::VISIBLE, so a saved "visible" flag cannot show the window early. The same mask is used byactualizar.rs→guardar_ventanabefore an update restart. The plugin is only added#[cfg(desktop)]and is a desktop-only dependency inCargo.toml. -
Three independent ways to show the window, so it can never stay hidden:
on_page_load(Rust shows it atPageLoadEvent::Finished),ensenarVentana()at the end ofsrc/main.tsx(callsgetCurrentWindow().show(), allowed by capabilitycore:window:allow-show), and a fallback thread insetupthat shows it after 3 s ifis_visible()is false. Why not wait for the first painted frame: WebKitGTK gives norequestAnimationFrameframes to a hidden window, which previously delayed the window by the 3 s fallback. The theme is applied to<html>before React mounts (ponerTemaAntesDeMontar), so showing early is flash-free. -
setupfailures abort the app.AppState::openerrors are mapped toStringand returned fromsetup;run()then printsProxy Builder no ha podido arrancar: …to stderr and exits with status 1 (no window to show an error in). -
Plugin order: opener, dialog, updater are registered first;
.manage(actualizar::Actualizador::default())registers the updater's own state. -
After the shell mounts,
src/features/shell/use-deck-inicial.tsstarts autosave (iniciarAutoguardado, 800 ms) and the plan store (iniciarPlan, 100 ms), thenabrirDeckInicialopens the last deck from settinglast_deck_idor creates one.
Logging is initialised in setup by registro::iniciar(&app_log_dir): appends to proxy-builder.log, tracing_subscriber::fmt to that file (no ANSI, no target, level INFO, try_init so a second init is harmless). Its stated purpose is proving that a saved deck downloads nothing: every Scryfall request is logged as a line with method, host, path and status (scryfall/client.rs). Failures to create the directory or file are ignored silently. Other modules log with tracing::info!/warn! (updater, themes, symbols, OCR models, external sources).
src-tauri/src/state.rs. Registered with app.manage(...), injected into commands as State<'_, AppState>, and fetched from AppHandle in spawned tasks (app.state::<AppState>()).
| Field | Type | Meaning |
|---|---|---|
db |
store::db::Db |
SQLite connection behind a Mutex; WAL, foreign_keys=ON, synchronous=NORMAL; migrations applied on open (MIGRACIONES 1–5; only append, never edit a shipped one). Long jobs release the lock between steps. |
blobs |
store::blobs::BlobStore |
Content-addressed image store under paths.data/blobs. |
paths |
Paths { data, cache } |
app_data_dir (library, images, resources) and app_cache_dir (deletable derivatives). |
scryfall |
Arc<Scryfall> |
Rate-limited, retrying client for api.scryfall.com and image downloads. |
externo |
Arc<fuentes::Externo> |
Allow-listed client for every non-Scryfall site. |
jobs |
jobs::Jobs |
Cancel flags of running jobs. |
miniaturas |
Mutex<HashMap<String,String>> |
Search-result thumbnails: SHA-256 of the URL to the URL. Filled by searches (commands/cards.rs → registrar_miniatura, resumir; cleared when it passes 5000 entries), read by the /t/ route. |
diseno |
diseno::recursos::Recursos |
Fonts, frame packs and symbols of the designer, loaded at startup from paths.data. |
vistas |
Mutex<diseno::vista::Vistas> |
Designs registered for live preview (/d/), capped at the 64 most recent. |
imagenes |
CacheImagenes |
Decoded artwork for design rendering. |
detalles |
biblioteca::CacheDetalles |
Library details per deck, in memory keyed by updated_at. |
renders |
tokio::sync::Semaphore |
Bounds concurrent thumbnail renders to available_parallelism() (fallback 4). |
mesa |
Mutex<Option<mesa::sesion::Sesion>> |
The game table session, if a game is open (Game Table). |
red |
red::Red |
The network: the installation key, the iroh node (started when needed), the P2P role (Sala), the room-server client and its room (Salon) (Online Play). |
mirada |
Mutex<Option<red::mirada::Mirada>> |
What a spectator sees (no session, only the view and its patches). |
trabajos (private) |
AtomicUsize |
Count of running long jobs; see trabajo() / ocupado(). ocupado() is true while a job runs or a game is open (hay_partida()), so neither blob GC nor an update (which restarts the app) runs then. |
AppState::open(paths) creates both directories, opens library.db, runs store::decks::rellenar_refs once (backfills blob_refs so designed images are protected from GC), builds the blob store and the designer resources, and the two HTTP clients. Tests build an AppState over tempfile directories (see the estado() helpers in render.rs, commands/cards.rs, diseno/*.rs).
Concurrency notes: db.conn() returns a MutexGuard or Internal if poisoned; hold it briefly. trabajo() returns a guard whose Drop decrements the counter, so early returns and panics-unwinding release it.
Registered with register_asynchronous_uri_scheme_protocol(protocol::ESQUEMA, protocol::manejador). The handler copies the AppHandle, spawns an async task per request, and answers via UriSchemeResponder. Only the URL path is used (peticion.uri().path()).
On the WebView side the base is convertFileSrc("", "pbimg"): pbimg://localhost/ on Linux/macOS and http://pbimg.localhost/ on Windows (src/lib/image-url.ts → raiz()), which is why the CSP lists both.
protocol.rs → leer_ruta parses the path into Ruta (and nothing else is accepted):
| Path | Ruta |
Returns | Produced by (src/lib/image-url.ts) |
|---|---|---|---|
/r/<sha256>/<params>/<width> |
Render { blob, params, ancho } |
JPEG thumbnail of a stored blob, processed with the deck's edits | urlMiniatura(blob, PreviewParams, ancho) |
/t/<sha256 of URL> |
Miniatura { huella } |
Image of a search result (PNG or JPEG, sniffed by magic bytes) | urlBusqueda(huella) |
/d/<huella>/<width> |
Diseno { huella, ancho } |
PNG preview of a design registered with design_register
|
urlDiseno |
/c/<huella>/<width> |
Capa { huella, ancho } |
PNG of the card without its artwork (design_overlay) |
urlCapa |
/b/<sha256>/<side> |
Arte { blob, lado } |
Artwork from the blob store, reduced so its long side is 1000 or 2000 px (JPEG, or PNG if it has transparency) | urlArte(blob, 1000 | 2000) |
/x/<pack>/<n> |
Extra { pack, indice } |
PNG extra image of a downloadable frame pack | urlExtra(pack, n) |
/f/<sha256>.woff2 |
Letra { huella } |
Font file of an installed tweakcn theme |
raizProtocolo() used by src/lib/temas-instalados.ts
|
/m/<sha256>/<width> |
Mesa { imagen: ImagenMesa::Propia, ancho } |
JPEG of a game table face prepared by the app (or a mat) | urlMesa({ tipo: "propia" }, ancho) |
/m/s/<scryfall id>/<face>/<width> |
Mesa { imagen: ImagenMesa::Scryfall, ancho } |
JPEG of an untouched Scryfall print used on the game table | urlMesa({ tipo: "scryfall" }, ancho) |
Validation, enforced and unit-tested (protocol.rs tests las_rutas_raras_se_rechazan and others):
-
<sha256>/<huella>must passstore::blobs::es_huella(exactly 64 lowercase hex characters), which also blocks path traversal. -
<width>must be one ofANCHOS = [146, 244, 488, 672, 745, 1000](mirrored byANCHOSinimage-url.ts);<side>one ofLADOS_ARTE = [1000, 2000](diseno/vista.rs). -
<params>is the base64url (no padding) of the JSONPreviewParams(render.rs):edits,edge(edge darkening),back,bleedMm,bleedMode,fillCorners,card(CardSize). Maximum 2048 characters (MAX_PARAMS), must decode and deserialize. -
<pack>is 1–40 characters of[A-Za-z0-9_-];<n>a non-negative integer. -
/m/widths must be one ofANCHOS_MESA(the six ofANCHOSplus 1920, for the mat);<scryfall id>must passmesa::imagenes::es_id_scryfall(36 characters, hex digits and dashes) and<face>is 0 or 1 (testlas_rutas_de_la_mesa). Files come fromapp_cache_dir/mesa/; widths of 745 or more are served as stored, smaller ones are reduced once intorenders/m/(mesa/imagenes.rs::servir). See Game Table. - Status codes: unparsable-but-recognised shape is
400, unknown shape404, missing blob/design404, other render failure500, failed remote fetch for/t/502. - Every response sets
X-Content-Type-Options: nosniffandAccess-Control-Allow-Origin: *; successful ones addCache-Control: max-age=31536000, immutable, because the URL contains everything that changes the image.
Why URLs instead of IPC bytes: no image bytes cross the IPC, and WebKit caches by URL. The URL carries all render parameters, so changing an edit or a setting simply produces a new URL.
Routes /r/, /d/, /c/, /b/ first await estado.renders.acquire() (semaphore sized to the core count), then run the CPU work in tauri::async_runtime::spawn_blocking. /x/ and /f/ are small file reads and skip the semaphore. /m/ runs in spawn_blocking without the semaphore: game images are prepared beforehand, so a request is a file read or, once per width, a resize. Because the front end only mounts <img> for visible pages (useVisible, see Live Preview), the semaphore queue stays short.
- Look for
app_cache_dir/renders/t/<hash>.jpg; serve it if present. - Else look the hash up in
AppState.miniaturas; unknown hash is404. - Download: URLs starting with
https://cards.scryfall.io/usescryfall.download; anything else goes throughexterno.bajar(url, MAX_IMAGEN)andfuentes::limpiar_imagen(decode and re-encode), so only allow-listed sources are ever fetched. - Write the file atomically into the cache and return it.
The WebView therefore never contacts any image host; the img-src CSP allows only 'self' pbimg: http://pbimg.localhost data: blob:.
src-tauri/src/render.rs turns a blob plus settings into pixels, for both the screen and the PDF.
| Item | Behaviour |
|---|---|
PreviewParams (#[derive(TS)]) |
What the screen puts in the URL. Rust decides whether edge darkening applies to this image from its origin and whether it is a back (proxy_core::imaging::pipeline::edge_for), the same rule as at export. |
spec_de_miniatura(blob, img, params, ancho) |
RenderSpec with target: SpecTarget::Width(ancho), JPEG quality 85. |
spec_de_casilla(slot, img, edits, settings) |
RenderSpec for a PagePlan slot at print resolution: card size is the slot's trim (so big cards work), bleed_mm is the slot's bleed_full_mm, SpecTarget::Dpi(quality.dpi()), encoding from the print settings; edits apply only if slot.apply_edits. |
AppState::carpeta_renders() |
paths.cache/renders. |
AppState::leer_blob(hash) |
Reads the file located by the blob store, probes size, reads origin from the blobs table; NotFound (param blob) if missing. |
AppState::miniatura(blob, params, ancho) |
Cache file renders/<RenderSpec::key()>.jpg; on a miss renders, then escribir_atomicos it (errors writing the cache are ignored). |
render_para_pdf(state, spec) |
Renders for print with no disk cache ("it weighs too much"). |
From<ImagingError> for AppError |
See IPC and Error Handling. |
RenderSpec::key() is the SHA-256 of (PIPELINE_VERSION, spec) serialized as JSON (proxy-core/src/imaging/pipeline.rs, PIPELINE_VERSION = 5). Any change in the spec or a bump of the pipeline version yields a different filename, which invalidates old cache files without deleting them (they are removed by cache_clear). The designer's PNG cache keys include its own engine version (proxy_core::diseno::VERSION_MOTOR).
Cache layout under app_cache_dir/renders/:
| Path | Content |
|---|---|
<spec key>.jpg |
Thumbnails of blobs |
t/<url hash>.jpg |
Search-result images (may be PNG bytes despite the extension; the protocol sniffs the type) |
d/<huella>-<width>.png |
Live design previews |
c/<huella>-<width>.png, c/<huella>.json
|
Design overlay and its artwork-window info |
b/<blob>-<side>.jpg or .png
|
Reduced artwork |
cache_stats and cache_clear (commands/library.rs) measure and remove_dir_all exactly this folder. Rule: only derivatives live here; deck images are never in this tree. A unit test in commands/library.rs asserts that the renders folder is under paths.cache and not under paths.data.
The generic atomic writer is store/blobs.rs → escribir_atomico(destino, bytes): create parent dirs, write <name>.<ext>.part, fsync, rename. A crash never leaves a half-written file under its final name. PDFs, package files, caches and blobs all use it.
| Key | Value / meaning |
|---|---|
productName, identifier, version
|
Proxy Builder, a reverse-DNS bundle identifier (read it in the file), 2.5.3 (kept in step with Cargo.toml and package.json; see Releases and Auto Updates) |
build |
beforeDevCommand: pnpm dev, devUrl: http://localhost:1420, beforeBuildCommand: pnpm build, frontendDist: ../dist
|
app.windows[0] |
label main, 1440x900, minimum 960x640, visible: false, backgroundColor: #1d1b19
|
app.security.csp |
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' pbimg: http://pbimg.localhost data: blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data: pbimg: http://pbimg.localhost; connect-src ipc: http://ipc.localhost; object-src 'none'; base-uri 'self'; form-action 'none'. devCsp is null (Vite HMR works in dev). |
bundle |
targets: all, createUpdaterArtifacts: true, category Utility; Debian package depends: [] (WebKitGTK is not forced as a dependency here) |
plugins.updater |
Minisign public key, endpoint https://github.com/PoolFlamingo/proxy-builder/releases/latest/download/latest.json, requireSignedVersion: true, Windows installMode: passive
|
Consequences of the CSP: the WebView cannot fetch anything outside IPC, cannot load remote scripts or images, and cannot submit forms. 'wasm-unsafe-eval' lets the WebView compile WebAssembly (the physics of the 3D dice, see Game Table); it does not allow eval of JavaScript. style-src 'unsafe-inline' is needed by React inline styles and the injected <style> for installed themes. Fonts for installed themes come from pbimg://…/f/…. The test la_configuracion_del_actualizador_es_valida in src-tauri/src/actualizar.rs reads tauri.conf.json and fails if updater signing, the endpoint shape or the public key header change.
Applies to window main and grants only: core:default, core:window:allow-show, core:window:allow-destroy, core:window:allow-set-fullscreen, opener:default, dialog:default, window-state:default. The description states the rule: no fs permission; all disk access goes through Rust commands. core:window:allow-show lets main.tsx reveal the window; core:window:allow-destroy lets the close guard (ConfirmarCierre, Settings and Shell) close it: with a JavaScript onCloseRequested listener Tauri stops closing the window by itself and destroy() does it, and that permission is not included in core:default / core:window:default, so without the explicit entry the window could not be closed at all (the build validates the identifier). core:window:allow-set-fullscreen is for the full-screen button of the game table (src/features/mesa/barra-mesa.tsx). The updater plugin has no capability entry because it is used only from Rust (actualizar.rs, commands/actualizar.rs). Custom commands registered with generate_handler! are not gated by a per-command permission list in this configuration (the app does not define a custom permission set; Inferred from the capability file containing only plugin and core permissions).
| Plugin (crate) | Used from | For |
|---|---|---|
tauri-plugin-dialog |
JS (src/lib/dialogos.ts) |
Open/save/folder dialogs |
tauri-plugin-opener |
Rust (OpenerExt in commands/export.rs, commands/temas.rs) |
Open exported files, reveal in folder, open the tweakcn community page |
tauri-plugin-updater |
Rust only (actualizar.rs) |
Check, download, verify signature, install |
tauri-plugin-deep-link (desktop) |
Rust only (enlace.rs, set up in lib.rs) |
proxybuilder:// invitation links; no capability entry because the screen never calls it |
tauri-plugin-single-instance (desktop, feature deep-link) |
Rust only (lib.rs), not with PB_PERFIL
|
A second launch hands its arguments (the link) to the open app |
tauri-plugin-window-state (desktop) |
Plugin + Rust save_window_state
|
Remember size, position, maximised; never visibility |
Other platform dependencies: gtk on Linux and arboard elsewhere, both for reading the clipboard image from Rust (diseno/portapapeles.rs; WebKitGTK refuses navigator.clipboard.read() for the web view).
-
[profile.dev] debug = "line-tables-only",split-debuginfo = "packed", dependencies withdebug = false: keepstarget/small. -
[profile.dev.package."*"] opt-level = 2: dependencies (image, resize, PDF) compile optimised even in dev because unoptimised image processing makes the preview unusable underpnpm tauri dev. -
reqwestusesrustlswithdefault-features = false; CI fails if OpenSSL appears incargo tree. - Game table dependencies:
getrandom(the seed of each game, from system entropy),regex(the fixed «+N/+N» of auras and equipment) andflate2with thezlib-rsbackend (the saved game; the same zlib-rs thatzipalready brings).
| Module | Responsibility | Deeper page |
|---|---|---|
commands/* |
Tauri commands, one file per area | IPC and Error Handling |
error.rs |
AppError, ErrorCode, From impls |
IPC and Error Handling |
state.rs, jobs.rs, registro.rs
|
Shared state, job flags, log file | this page |
protocol.rs, render.rs
|
pbimg:// and renders |
this page, Live Preview |
store/ (db, blobs, decks, presets, settings, colecciones, etiquetas, migrations/) |
SQLite library and blob store | Storage and Blobs |
package.rs |
.pbdeck ZIP packages |
Deck Package Format |
biblioteca.rs |
Per-deck details for the library (colors, class, price, languages), cached by updated_at
|
Deck Library |
scryfall/ (client, rate, cache, models, resolve, busquedas, fichas, fichas_auto, nombres) |
Scryfall access, name catalogue, list resolution | Scryfall Integration |
archidekt.rs |
Import from an Archidekt URL (exact Scryfall ids) | Scryfall Integration |
fuentes/ (cockatrice, mtgio, mtgjson, mod.rs with Externo) |
Non-Scryfall sources behind an allow-list | External Sources |
ocr.rs, originales.rs
|
Optional OCR and original-name detection | Card Identification and OCR |
diseno/ |
App side of the card designer (drafts, rendering with resources, live preview, artwork, PNG export, batch, packs and fonts download, clipboard) | Designer App Layer |
temas.rs |
tweakcn themes and fonts | Themes and Skins |
actualizar.rs |
Updater wrapper | Releases and Auto Updates |
enlace.rs |
Invitation links proxybuilder://unirse/<code>: validation, enlace_pendiente, enlace_invitacion
|
Online Play |
mesa/ (sesion, preparar, imagenes, guardada, tapete, ejemplo) |
The game table session around the mesa crate, deck and image preparation, the saved game, the mat |
Game Table |
red/ (perfil, identidad, nodo, protocolo, codigo, anfitrion, invitado, mirada, imagenes, latencia, mesa, colores, servidor/) and commands/{red,salas}.rs
|
Online play: debug profiles, identity, iroh node, game protocol, direct code, host and guest, spectators, images over the network, the room-server client and the room in the app | Online Play |
-
Never block the async runtime with CPU work. Use
spawn_blocking(asexport_pdf, the protocol handlers anden_hiloincommands/diseno.rsdo). -
Hold
state.trabajo()for every long job orblob_gcandupdate_installmay run under it. An open game counts as busy by itself (hay_partida()). -
cache_clearmust only touchrenders/. A new cache location for deck-critical data would break this guarantee; put it inapp_data_dirand the blob store instead. -
Add new protocol routes with a test in
protocol.rsfor both acceptance and rejection (traversal, bad hash length, uppercase hex, bad widths are already covered). -
Do not edit shipped migrations; append
0006_*.sqland add it toMIGRACIONESinstore/db.rs. -
Blob store versus cache: the blob store is in
app_data_dirbecause decks depend on it; GC keeps images younger than 24 h (GRACIA_MS) and anything referenced bydeck_cards,decks.default_back_blob,blob_refs, or the generic back setting. -
Release builds hide the console on Windows; use the log file in
app_log_dirto debug network behaviour.
| Where | What |
|---|---|
src-tauri/src/protocol.rs (mod tests) |
Valid paths parse; malformed, traversal, wrong widths/sides, bad pack names, .ttf instead of .woff2 are rejected; game table routes (las_rutas_de_la_mesa) |
src-tauri/src/mesa/ (sesion.rs, guardada.rs, preparar.rs) |
Patches equal the full view, rejections carry their reason, restart, an open game makes the app busy, save and resume keep the fingerprint, the second preparation paints nothing |
src-tauri/src/render.rs (mod tests) |
RenderSpec building from slots, error conversion |
src-tauri/src/jobs.rs |
alta_cancelar_y_baja |
src-tauri/src/error.rs |
Error serialisation |
src-tauri/src/actualizar.rs |
Updater config in tauri.conf.json is valid |
src-tauri/src/commands/library.rs |
Cache folder is under cache, not data |
src-tauri/src/commands/carpetas.rs |
la_carpeta_de_una_ruta |
- New command or job: IPC and Error Handling.
- New shared resource: add a field to
AppStateand initialise it inAppState::open; keep tests constructing it throughPathson temp directories. - New plugin: add the crate,
.plugin(...)inlib.rs, a permission incapabilities/default.jsononly if the WebView uses it, and update the CSP only if it needs new origins (avoid; prefer a Rust command).
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