Repository navigation
Arquitectura
English · Español
Proxy Builder es una aplicación de escritorio Tauri 2: un frontend React/TypeScript que solo pinta pantallas y guarda el estado de pantalla, un crate de la app (proxy-builder) que se encarga de la E/S (SQLite, disco, red, Tauri) y un crate puro (proxy-core) que se encarga de todo cálculo determinista (parseo, geometría de página, procesado de imagen, PDF, estadísticas, motor de diseño de cartas). Esta página es la vista de todo el sistema; cada caja enlaza a la página que profundiza en ella.
| Capa | Dónde | Se encarga de | No debe |
|---|---|---|---|
| Frontend React | src/ |
Pintado, estado de pantalla (stores de Zustand), i18n, temas | Calcular geometría, hablar con la red, llamar a invoke directamente |
Crate de la app proxy-builder (lib proxy_builder_lib) |
src-tauri/src/ |
Cableado de Tauri, comandos, biblioteca SQLite, almacén de blobs, Scryfall y otras fuentes, cachés, trabajos, protocolo pbimg://, actualizador |
Contener lógica pura que podría probarse sin Tauri |
Crate core proxy-core
|
src-tauri/crates/proxy-core/src/ |
model, decklist, layout, imaging, pdf, stats, diseno (motor del diseñador de cartas), paper, units
|
Conocer Tauri, la red, la base de datos ni AppError
|
Crate del motor de la mesa mesa
|
src-tauri/crates/mesa/src/ |
La mesa de juego: modelo, catálogo de acciones, azar determinista, visibilidad, deshacer y repetición (véase Motor de la mesa) | Lo mismo que proxy-core, y además reloj o azar de fuera |
| Servidor de salas (repositorio aparte) |
server/ (ignorado por la app) |
Identidad sin cuentas, playgroups, invitaciones, salas, señalización, elección y migración del host, el callout del relé (ver Juego en línea) | Llevar la partida: las partidas son P2P entre las apps |
Los crates de Rust son miembros de un mismo workspace de Cargo declarado en src-tauri/Cargo.toml (members = [".", "crates/proxy-core", "crates/mesa"]). proxy-core tiene sus propios enums de error (LayoutError, ImagingError, PdfError, ProbeError, DisenoError, PackError, LineError) y la app los convierte a AppError con implementaciones de From (véase IPC y manejo de errores).
flowchart TB
subgraph WV["WebView (React 19, sin red)"]
UI["features/* (decklist, preview, export, variants, library, disenar, ...)"]
ST["stores/* (deck-store, plan-store, ui-store, trabajo-store, actualizacion-store, edicion-store)"]
IPC["lib/ipc.ts: call() y on()"]
IMG["lib/image-url.ts: URLs pbimg://"]
UI --> ST
UI --> IPC
UI --> IMG
end
subgraph APP["Crate de la app proxy-builder (src-tauri/src)"]
LIB["lib.rs: run(), plugins, setup, generate_handler!"]
CMD["commands/*.rs (108 comandos)"]
PROTO["protocol.rs + render.rs (pbimg://)"]
STATE["state.rs: AppState"]
STORE["store/*: Db (SQLite), BlobStore, settings, presets"]
SCRY["scryfall/*: client, cache, resolve, nombres"]
FUE["fuentes/*: lista permitida Externo, Cockatrice, MTGJSON, MTG.io"]
DIS["diseno/*: capa de app del diseñador"]
MISC["package.rs, ocr.rs, originales.rs, temas.rs, actualizar.rs, biblioteca.rs"]
LIB --> CMD
LIB --> PROTO
CMD --> STATE
PROTO --> STATE
STATE --> STORE
STATE --> SCRY
STATE --> FUE
STATE --> DIS
end
subgraph CORE["Crate core proxy-core (puro)"]
MODEL["model, units, paper"]
DL["decklist"]
LAY["layout: PagePlan"]
IM["imaging"]
PDF["pdf"]
STA["stats"]
DSN["diseno: motor"]
end
IPC -- "invoke / eventos" --> CMD
IMG -- "GET pbimg://localhost/..." --> PROTO
CMD --> CORE
PROTO --> CORE
SCRY -- HTTPS --> NET1["api.scryfall.com, cards.scryfall.io"]
FUE -- "HTTPS, solo lista permitida" --> NET2["Cockatrice, MTGJSON, MTG.io, tweakcn, GitHub"]
STORE --> DISK1[("app_data_dir")]
PROTO --> DISK2[("app_cache_dir")]
Vienen de CLAUDE.md; cada una es un invariante que la revisión de código y el CI hacen cumplir o del que depende la organización del código.
| Decisión | Motivo | Dónde se ve |
|---|---|---|
| Todo lo pesado vive en Rust: parseo de la lista, Scryfall, descargas, cachés, geometría de página, procesado de imagen, PDF, estadísticas. React solo pinta y guarda el estado de pantalla. | Una sola implementación, probable con cargo test, y el WebView nunca se bloquea con trabajo de CPU ni toca la red. |
Módulos de proxy-core; src/lib/ipc.ts como único puente |
Una sola geometría para pantalla y papel. PagePlan (milímetros) lo calcula una vez proxy_core::layout::plan y lo consumen tanto la vista previa como el compositor del PDF. La pantalla nunca recalcula posiciones. |
Lo que ves es lo que se imprime. Una segunda implementación de la maquetación acabaría divergiendo. |
commands/layout.rs → compute_page_plan; stores/plan-store.ts → usePlan; commands/export.rs → exportar llama a plan(deck); doc de proxy-core/src/pdf/mod.rs: «Este módulo no calcula posiciones: dibuja el PagePlan» |
Una sola canalización de imagen para pantalla y papel. Las miniaturas y las imágenes del PDF pasan por el mismo render() con un RenderSpec. |
El mismo motivo; fidelidad de la vista previa. |
src-tauri/src/render.rs → spec_de_miniatura, spec_de_casilla; proxy-core/src/imaging/pipeline.rs
|
Las imágenes de los mazos viven en el almacén de blobs (app_data_dir/blobs/, direccionado por SHA-256), nunca en la caché. La caché (app_cache_dir/renders/) solo guarda derivados y se puede borrar sin perder nada. |
Los mazos dependen de sus imágenes; el sistema operativo puede vaciar las cachés. cache_clear nunca debe perder datos del usuario. |
store/blobs.rs → BlobStore; render.rs → AppState::carpeta_renders; commands/library.rs → cache_clear (un test comprueba que renders está bajo paths.cache y no bajo paths.data) |
La vista previa desmonta las imágenes de las páginas que no se ven (IntersectionObserver cuyo root es el viewport del ScrollArea). |
Solo las cartas visibles las renderiza Rust y las retiene WebKit. El documento en sí nunca hace scroll, así que un observer con raíz en el documento consideraría visible todo. |
src/features/preview/visibilidad.tsx → useVisible, RaizScroll
|
proxy-core es puro: sin Tauri, sin red, sin base de datos. |
Tests rápidos (cargo test -p proxy-core), límite claro. |
Comentario de doc de proxy-core/src/lib.rs; Cargo.toml del crate (sin tauri, reqwest ni rusqlite) |
lib.rs solo arranca y registra; cada área tiene su commands/<área>.rs. |
El lib.rs de un proyecto anterior creció hasta unas 12k líneas; el fichero se mantiene pequeño a propósito. |
src-tauri/src/lib.rs (unas 200 líneas, su comentario de doc lo dice) |
Los comandos devuelven Result<T, AppError> ({ code, params }); la pantalla traduce con useLang().mensaje(e). Rust nunca escribe prosa para el usuario. |
El idioma puede cambiar sin tocar Rust; un solo sitio para los textos. |
src-tauri/src/error.rs; src/lib/errores.ts
|
Los tipos compartidos vienen de Rust: #[derive(TS)] #[ts(export)] y pnpm bindings escriben src/bindings/. No se editan a mano. |
Es imposible que el desfase de tipos entre frontend y backend llegue sin que se note. |
.cargo/config.toml define TS_RS_EXPORT_DIR
|
La pantalla habla con Rust solo por call() de src/lib/ipc.ts. |
La interfaz Commands hace que el compilador compruebe los nombres de comando y los tipos de argumentos y resultado. |
src/lib/ipc.ts |
| El WebView nunca sale a internet; toda la red la hace Rust. | Un único sitio para limitar la frecuencia, las listas permitidas, la redacción y el registro; CSP estricta. | CSP de tauri.conf.json (connect-src es solo ipc:); scryfall/client.rs; fuentes::Externo
|
Todo lo que no es Scryfall se descarga por fuentes::Externo: lista permitida cerrada de sitio y ruta, cada redirección se vuelve a comprobar, topes de tamaño, imágenes siempre decodificadas y recodificadas. |
Limita el radio de daño de un sitio de terceros comprometido o cambiado. | Fuentes externas |
| Los cambios al árbol del mantenedor llegan como ediciones sin commit; el flujo en la nube nunca hace push. | Regla de proceso, no de código. | Véanse Convenciones y Tests y CI |
Hay un único proceso del sistema operativo (la app de Tauri) con tres contextos de ejecución:
-
Proceso principal de Rust. Bucle de eventos de Tauri, manejadores de plugins,
AppState. Las funciones#[tauri::command]síncronas se ejecutan en el lado de Tauri; los comandosasyncse ejecutan en el runtime asíncrono de Tauri (tokio). El trabajo largo se pasa atauri::async_runtime::spawn(trabajos limitados por red:resolve_decklist,identify_batch) o aspawn_blocking(limitados por CPU:export_pdf). Dentro de un trabajo, los pools de hilos de rayon hacen el trabajo en paralelo por imagen (la exportación a PDF construye su propio pool de como mucho 8 hilos encommands/export.rs→exportar). -
El WebView (WebKitGTK en Linux, WebView2 en Windows, WKWebView en macOS). Carga
dist/(o el servidor de desarrollo de Vite en el puerto 1420). Su CSP prohíbe cualquier red salvo IPC (connect-src: ipc: http://ipc.localhost). Las imágenes vienen del esquema propiopbimg:. -
Hilos y pools de trabajo dentro de Rust (rayon,
spawn_blocking, el semáfororendersque limita los renders de miniaturas simultáneos al número de núcleos,AppState.renders).
El WebView y Rust se comunican exactamente por tres canales:
| Canal | Dirección | Mecanismo | Página |
|---|---|---|---|
| Comandos | WebView a Rust, petición/respuesta |
invoke envuelto por call()
|
IPC y manejo de errores |
| Eventos | Rust al WebView, sin esperar respuesta |
app.emit("<job>://progress" | "<job>://done", payload) escuchado por on()
|
IPC y manejo de errores |
| Protocolo de imagen | WebView a Rust, GET |
pbimg:// (http://pbimg.localhost/ en Windows), servido por protocol.rs
|
Capa de aplicación Rust |
sequenceDiagram
actor U as Usuario
participant P as Panel de la lista (acciones.ts)
participant IPC as ipc.ts call/on
participant R as Comandos de Rust
participant SC as Cliente de Scryfall + BlobStore
participant DS as deck-store
participant PS as plan-store
participant PC as proxy-core
participant PV as Vista previa (slot-view.tsx)
participant PR as protocolo pbimg
U->>P: pega la lista, pulsa Cargar
P->>IPC: call parse_decklist {text}
IPC->>R: invoke
R->>PC: decklist::parse
PC-->>P: ParsedDecklist
P->>IPC: lanzarTrabajo(resolve://progress, resolve://done)
IPC->>R: call resolve_decklist {list, options}
R-->>P: jobId (de inmediato)
R->>SC: lotes de /cards/collection, idioma, fichas
SC->>SC: imágenes a blobs/ (SHA-256)
R-->>P: evento resolve://progress {jobId, progress}
R-->>P: evento resolve://done {jobId, result, error}
P->>DS: aplicar(cards) (entrada de historial, version++)
DS->>PS: se dispara la suscripción (100 ms de espera)
PS->>IPC: call compute_page_plan {deck}
IPC->>R: invoke
R->>PC: layout::plan(deck)
PC-->>PS: PagePlan (mm)
PS-->>PV: páginas y casillas
PV->>PV: useVisible(page) true
PV->>PR: GET pbimg://localhost/r/{blob}/{params}/{width}
PR->>PC: imaging::pipeline::render (en caché en app_cache_dir/renders)
PR-->>PV: JPEG
DS->>IPC: call deck_save (800 ms tras el último cambio)
U->>P: Exportar PDF, elige una ruta (plugin dialog)
P->>IPC: lanzarTrabajo(export://progress, export://done)
IPC->>R: call export_pdf {deck snapshot, path}
R-->>P: jobId
R->>PC: layout::plan, renderiza cada imagen distinta (rayon), pdf::compose
R-->>P: export://progress {phase: prepare / compose / write}
R-->>P: export://done {jobId, result, error}
Puntos clave que se ven en el diagrama:
-
resolve_decklistyexport_pdfdevuelven unjobIdal instante y terminan por evento; el frontend escucha antes de lanzar (src/lib/trabajos.ts→lanzarTrabajo) para que un trabajo rápido no pueda terminar sin que nadie lo vea. - La exportación envía la instantánea del mazo que hay en el store, no la que hay en SQLite, porque el autoguardado puede ir hasta 800 ms por detrás (
src/features/export/acciones.ts→exportarA). - La vista previa y el PDF ejecutan el mismo
render(); la vista previa pide un ancho deANCHOS = [146, 244, 488, 672, 745, 1000], el PDF pide los PPP de la calidad elegida.
Páginas más detalladas: Parseo de la lista del mazo, Integración con Scryfall, Motor de maquetación, Vista previa en vivo, Imagen y PDF, Exportación.
La app pide a Tauri los directorios en el setup de lib.rs y los guarda en state::Paths { data, cache }; el directorio de registros viene de app_log_dir().
| Ubicación | Contenido | Origen | ¿Se puede borrar sin perder datos? |
|---|---|---|---|
app_data_dir/library.db |
SQLite (WAL): decks, deck_cards, blobs, blob_refs, presets, app_settings, scryfall_cards, tags, deck_tags, deck_listas, partidas, partida_decks, partida_imagenes; migraciones 0001–0008 en src-tauri/src/store/migrations/
|
state.rs → AppState::open, store/db.rs → Db
|
No |
app_data_dir/blobs/<2 hex>/<sha256>.<ext> |
Todas las imágenes que usa un mazo (PNG de Scryfall, subidas, generadas, diseñadas) |
store/blobs.rs → BlobStore
|
No |
app_data_dir/fuentes/, marcos/, simbolos/
|
Recursos del diseñador: letras importadas, packs de marcos (incluidos los packs descargados y los marcos «Original»), símbolos de maná |
diseno/recursos.rs → Recursos
|
Se pueden volver a descargar, pero se pierden las importaciones del usuario |
app_data_dir/letras-cjk/ |
Noto Sans JP/KR/SC/TC (<código>.ttf) y su licencia OFL, bajadas la primera vez que un deck usa el japonés, el coreano o el chino como idioma de diseño |
diseno/letras_cjk.rs, las lee diseno/recursos.rs
|
Sí (se vuelven a bajar y se comprueba su SHA-256) |
app_data_dir/modelos/ |
Modelos de OCR, descargados la primera vez que se usa el OCR | ocr.rs |
Sí (se vuelven a descargar) |
app_data_dir/temas/ |
Temas de tweakcn instalados (<id>.json) y sus letras (fuentes/<hash>.woff2) |
temas.rs |
Se desinstalan los temas |
app_data_dir/partidas/actual.json.gz, partidas/imagenes/
|
La partida en curso de la mesa de juego; copias de las imágenes propias que pintan las repeticiones del historial |
mesa/guardada.rs, mesa/imagenes.rs (Mesa de juego) |
Se pierde la partida en curso; las repeticiones del historial pierden esas imágenes |
app_data_dir/iconos/elegidos.json; app_cache_dir/iconos/
|
Iconos de Iconify elegidos por la persona (guardados para verlos sin red); el catálogo del selector, renovado cada semana |
iconos.rs (Iconos) |
La caché sí; los elegidos vuelven a necesitar la red |
app_cache_dir/renders/ |
JPEG de miniaturas con clave RenderSpec::key(); t/ miniaturas de resultados de búsqueda; b/ ilustración reducida |
render.rs, protocol.rs, diseno/vista.rs
|
Sí (cache_clear) |
app_cache_dir/scryfall-card-names.json, scryfall-set-codes.json
|
Catálogos de nombres y de códigos de set de Scryfall, renovados cada semana | scryfall/nombres.rs |
Sí |
app_cache_dir/cockatrice-tokens.xml; app_cache_dir/mtgjson/nombres.sqlite
|
Catálogo de fichas y el índice de nombres en varios idiomas |
fuentes/cockatrice.rs, fuentes/mtgjson.rs
|
Sí |
app_log_dir/proxy-builder.log |
Una línea por petición a Scryfall (método, host, ruta, estado) |
registro.rs, scryfall/client.rs
|
Sí |
Almacenamiento del navegador (localStorage) |
Clave del idioma de la interfaz proxybuilder.lang, otras comodidades por espectador |
src/components/language-provider.tsx |
Sí |
Obsérvese que la caché de datos de cartas de Scryfall no está en app_cache_dir: es la tabla scryfall_cards dentro de library.db (scryfall/cache.rs, TTL de una semana), de modo que las estadísticas y la biblioteca funcionan sin conexión. Como cache_clear solo vacía renders/, esa tabla sobrevive.
Véase Almacenamiento y blobs para las tablas y la recolección de basura.
Las versiones son los rangos declarados en los manifiestos; donde importa se indican las versiones resueltas de src-tauri/Cargo.lock.
| Área | Tecnología | Declarada | Resuelta / nota |
|---|---|---|---|
| Shell | Tauri | tauri = "2" |
2.12.3 |
| Plugins |
tauri-plugin-dialog, -opener, -updater, -window-state
|
"2" |
2.8.2, 2.7.1, 2.14.0, 2.5.0 (window-state solo en escritorio) |
| Tipos | ts-rs |
"12" |
12.0.1 |
| Base de datos |
rusqlite con SQLite bundled
|
"0.40" |
0.40.2 |
| HTTP |
reqwest (rustls, sin OpenSSL; el CI comprueba que cargo tree -i openssl esté vacío) |
"0.13" |
0.13.5 |
| Asíncrono |
tokio (rt, sync, time, macros) |
"1" |
1.53.2 |
| Paralelismo | rayon |
"1.12" |
|
krilla (raster-images, simple-text) |
"0.8.2" |
0.8.2 | |
| Imágenes |
image, fast_image_resize (Lanczos3, SIMD) |
0.25.10, 6.1.0
|
|
| Renderizado de cartas |
parley (texto), tiny-skia, resvg, skrifa
|
0.12, 0.12, 0.48, 0.44
|
0.12.0, 0.12.0, 0.48.1 |
| OCR |
ocrs + rten
|
0.13, 0.26
|
0.13.1, 0.26.0 |
| Paquetes |
zip (solo deflate) para .pbdeck
|
"9" |
|
| Registro |
tracing, tracing-subscriber (fmt) |
0.1, 0.3
|
|
| Edición de Rust / MSRV | 2021, rust-version = "1.92"
|
||
| Frontend | React, React DOM | ^19.1.0 |
|
| Estado | Zustand | ^5.0.15 |
|
| Kit de UI | Radix UI (radix-ui ^1.5.0), lucide-react ^1.17.0 (solo en components/ui/), @iconify/utils + @iconify/json (véase Iconos), sonner ^2.0.7, react-resizable-panels ^4.11.2, @dnd-kit/core ^6.3.1
|
||
| Estilos | Tailwind CSS 4 (@tailwindcss/vite), tw-animate-css
|
^4.3.0 |
|
| Compilación | Vite ^7.3.6, TypeScript ~5.8.3
|
||
| Tests / lint | Vitest ^5.0.1 (happy-dom), Biome 2.4.13, knip ^6.34.0
|
||
| Gestor de paquetes | pnpm 12.14.0 (packageManager), minimumReleaseAge: 1440 en pnpm-workspace.yaml
|
||
| Node | >=22 |
-
Una nueva capacidad del backend: pon la parte pura en
proxy-core(con su propio enum de error), la parte de E/S en el crate de la app, añadecommands/<área>.rs, regístralo en elgenerate_handler!delib.rs, añade su firma aCommandsensrc/lib/ipc.tsy ejecutapnpm bindings. Lista de comprobación en IPC y manejo de errores. -
Un nuevo trabajo largo: reserva con
state.jobs.alta(), manténstate.trabajo()durante todo el trabajo, emite<name>://progressy<name>://done, añade ambos aEventsenipc.tsy lánzalo desde la pantalla conlanzarTrabajo. -
Una nueva forma de URL de imagen: amplía
Rutayleer_rutaenprotocol.rs(con un test de rechazo) y añade un constructor ensrc/lib/image-url.ts.
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
- Icons · Iconos
- 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