Repository navigation
Capa de aplicación Rust
English · Español
El crate de la app (proxy-builder, librería proxy_builder_lib, en src-tauri/) conecta Tauri con el crate puro proxy-core: construye la ventana y los plugins, es dueño de AppState (base de datos, almacén de blobs, clientes HTTP, trabajos), sirve el protocolo de imagen pbimg:// y registra todos los comandos. Esta página cubre la secuencia de arranque, el estado compartido, el protocolo y la caché de renders, la configuración de Tauri (ventana, CSP, capabilities, plugins), la capa que hace que el mismo crate funcione en Android y un mapa de módulos del crate.
| Fichero | Función |
|---|---|
src-tauri/src/main.rs |
Seis líneas: fn main() { proxy_builder_lib::run() }. En las compilaciones release de Windows, #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] suprime la consola. |
src-tauri/src/lib.rs |
pub fn run() (marcada con #[cfg_attr(mobile, tauri::mobile_entry_point)]): builder, plugins, protocolo, setup, generate_handler!. También declara todos los módulos y #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]. |
src-tauri/build.rs |
tauri_build::build(). |
src-tauri/Cargo.toml |
Tipos de crate de la librería staticlib, cdylib, rlib; el nombre de la librería lleva el sufijo _lib para evitar un choque de nombres en Windows con el binario. Miembros del workspace: la app y crates/proxy-core. Ajustes del perfil de desarrollo (véase más abajo). |
lib.rs es pequeño a propósito: solo arranca y registra. Si crece, mueve código a un módulo bajo commands/ o a otro sitio.
sequenceDiagram
participant OS
participant RUN as lib.rs run()
participant TAU as Runtime de Tauri
participant ST as closure de setup
participant WV as WebView (ventana main, oculta)
participant FE as main.tsx
OS->>RUN: arranca proxy_builder_lib::run
RUN->>TAU: Builder: plugins opener, dialog, updater, window-state
RUN->>TAU: manage(Actualizador)
RUN->>TAU: on_page_load(muestra la ventana al terminar, 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 + migraciones, rellenar_refs, BlobStore, Recursos, Scryfall, Externo
ST->>TAU: manage(AppState)
ST->>ST: mesa::historial::instalar (guarda las partidas terminadas en el historial y copia las imágenes de su repetición)
ST->>ST: lanza una tarea: original::completar (marcos Original que falten)
ST->>ST: lanza un hilo: tras 3 s muestra la ventana si sigue oculta
TAU->>WV: crea la ventana (visible false, fondo 1d1b19)
WV->>FE: carga index.html y JS
FE->>FE: prepararTemasInstalados, ponerTemaAntesDeMontar
FE->>WV: pinta el árbol de React
FE->>TAU: getCurrentWindow().show()
TAU-->>WV: ventana mostrada (fondo oscuro, tema ya aplicado)
Note over RUN,TAU: Rust también la muestra en PageLoadEvent::Finished
Detalles y motivos:
-
La ventana arranca oculta y oscura.
tauri.conf.jsonfija"visible": falsey"backgroundColor": "#1d1b19". Así se evita un destello blanco mientras carga el WebView. -
El plugin window-state no debe hacerla visible.
tauri_plugin_window_state::Builderse configura conStateFlags::all() & !StateFlags::VISIBLE, de modo que un flag «visible» guardado no pueda mostrar la ventana antes de tiempo. La misma máscara la usaactualizar.rs→guardar_ventanaantes de reiniciar para actualizar. El plugin solo se añade con#[cfg(desktop)]y es una dependencia solo de escritorio enCargo.toml(Android no tiene ventana que recordar). -
Tres formas independientes de mostrar la ventana, para que nunca pueda quedarse oculta:
on_page_load(Rust la muestra enPageLoadEvent::Finished),ensenarVentana()al final desrc/main.tsx(llama agetCurrentWindow().show(), permitido por la capabilitycore:window:allow-show) y un hilo de reserva ensetupque la muestra a los 3 s siis_visible()es falso. Por qué no esperar al primer fotograma pintado: WebKitGTK no da fotogramas derequestAnimationFramea una ventana oculta, lo que antes retrasaba la ventana hasta el respaldo de 3 s. El tema se aplica a<html>antes de que React monte (ponerTemaAntesDeMontar), así que mostrarla pronto no produce destellos. -
Los fallos de
setupabortan la app. Los errores deAppState::opense convierten aStringy se devuelven desdesetup;run()entonces imprimeTapete no ha podido arrancar: …por stderr y sale con estado 1 (no hay ventana donde mostrar un error). -
Orden de los plugins: en escritorio va primero single-instance (el plugin lo exige, y no con
PB_PERFIL); luego opener, dialog,fs(usado solo desde Rust, para leer y escribir uncontent://) y updater;.manage(actualizar::Actualizador::default())registra el estado propio del actualizador; en móvil siguenandroid-extraybarcode-scanner; deep-link va el último. La ventana principal no la crea Tauri desde la configuración:tauri.conf.jsonla tiene concreate: falseycrear_ventanadelib.rsla construye con esa configuración ensetup, para que un perfil dePB_PERFILpueda darle su propia carpeta de datos del WebView y su título. -
Cuando el shell está montado,
src/features/shell/use-deck-inicial.tsarranca el autoguardado (iniciarAutoguardado, 800 ms) y el store del plan (iniciarPlan, 100 ms), y despuésabrirDeckInicialabre el último mazo del ajustelast_deck_ido crea uno.
El registro se inicializa en setup con registro::iniciar(&app_log_dir): añade a proxy-builder.log, con tracing_subscriber::fmt hacia ese fichero (sin ANSI, sin target, nivel INFO, try_init para que un segundo inicio sea inofensivo). Su propósito declarado es demostrar que un mazo guardado no descarga nada: cada petición a Scryfall se registra como una línea con método, host, ruta y estado (scryfall/client.rs). Los fallos al crear el directorio o el fichero se ignoran en silencio. Otros módulos registran con tracing::info!/warn! (actualizador, temas, símbolos, modelos de OCR, fuentes externas).
src-tauri/src/state.rs. Se registra con app.manage(...), se inyecta en los comandos como State<'_, AppState> y se obtiene desde el AppHandle en las tareas lanzadas (app.state::<AppState>()).
| Campo | Tipo | Significado |
|---|---|---|
db |
store::db::Db |
Conexión SQLite tras un Mutex; WAL, foreign_keys=ON, synchronous=NORMAL; migraciones aplicadas al abrir (MIGRACIONES 1–9; solo se añaden, nunca se edita una ya publicada). Los trabajos largos sueltan el lock entre pasos. |
blobs |
store::blobs::BlobStore |
Almacén de imágenes direccionado por contenido bajo paths.data/blobs. |
paths |
Paths { data, cache } |
app_data_dir (biblioteca, imágenes, recursos) y app_cache_dir (derivados borrables). |
scryfall |
Arc<Scryfall> |
Cliente con límite de frecuencia y reintentos para api.scryfall.com y las descargas de imágenes. |
externo |
Arc<fuentes::Externo> |
Cliente con lista permitida para todo sitio que no sea Scryfall. |
jobs |
jobs::Jobs |
Flags de cancelación de los trabajos en marcha. |
miniaturas |
Mutex<HashMap<String,String>> |
Miniaturas de resultados de búsqueda: SHA-256 de la URL a la URL. Lo rellenan las búsquedas (commands/cards.rs → registrar_miniatura, resumir; se vacía al pasar de 5000 entradas) y lo lee la ruta /t/. |
diseno |
diseno::recursos::Recursos |
Letras, packs de marcos y símbolos del diseñador, cargados al arrancar desde paths.data. |
vistas |
Mutex<diseno::vista::Vistas> |
Diseños registrados para la vista previa en vivo (/d/), con tope de los 64 más recientes. |
imagenes |
CacheImagenes |
Ilustraciones decodificadas para pintar diseños. |
detalles |
biblioteca::CacheDetalles |
Detalles de la biblioteca por mazo, en memoria con clave updated_at. |
renders |
tokio::sync::Semaphore |
Limita los renders de miniaturas simultáneos a available_parallelism() (4 si falla). |
mesa |
Mutex<Option<mesa::sesion::Sesion>> |
La sesión de la mesa de juego, si hay una partida abierta (Mesa de juego). |
red |
red::Red |
La red: la clave de la instalación, el nodo de iroh (se levanta al hacer falta), y el papel P2P (Sala) (Juego en línea). |
mirada |
Mutex<Option<red::mirada::Mirada>> |
Lo que ve un espectador (sin sesión, solo la vista y sus parches). |
pasar |
pasar::Pasar |
Lo que está en marcha al pasar decks entre equipos: la búsqueda de equipos cerca, el lado que recibe y el que envía (Pasar decks). |
amigos |
amigos::Amigos |
Amigos: el cliente HTTP del servidor de Tapete (se crea al primer uso), el último estado enseñado a la pantalla, los amigos tal como los describió el servidor la última vez (las claves para cifrarles), si hay un chat abierto o la app está en segundo plano, y el cerrojo de una vuelta a la vez del bucle de sincronización (Amigos y mensajes). |
trabajos (privado) |
AtomicUsize |
Número de trabajos largos en marcha; véanse trabajo() / ocupado(). ocupado() es verdadero mientras corre un trabajo o hay una partida abierta (hay_partida()), así no corren ni la limpieza de blobs ni una actualización (que reinicia la app). |
AppState::open(paths) crea ambos directorios, abre library.db, ejecuta una vez store::decks::rellenar_refs (rellena blob_refs para que la GC proteja las imágenes diseñadas), construye el almacén de blobs y los recursos del diseñador, y los dos clientes HTTP. Los tests construyen un AppState sobre directorios de tempfile (véanse los ayudantes estado() en render.rs, commands/cards.rs, diseno/*.rs).
Notas de concurrencia: db.conn() devuelve un MutexGuard o Internal si está envenenado; mantenlo poco tiempo. trabajo() devuelve una guarda cuyo Drop decrementa el contador, de modo que los retornos tempranos y el desenrollado por pánico la liberan.
Se registra con register_asynchronous_uri_scheme_protocol(protocol::ESQUEMA, protocol::manejador). El manejador copia el AppHandle, lanza una tarea asíncrona por petición y responde mediante UriSchemeResponder. Solo se usa la ruta de la URL (peticion.uri().path()).
En el lado del WebView la base es convertFileSrc("", "pbimg"): pbimg://localhost/ en Linux/macOS y http://pbimg.localhost/ en Windows (src/lib/image-url.ts → raiz()), por lo que la CSP lista ambas.
protocol.rs → leer_ruta convierte la ruta en Ruta (y no se acepta nada más):
| Ruta | Ruta |
Devuelve | Producido por (src/lib/image-url.ts) |
|---|---|---|---|
/r/<sha256>/<params>/<width> |
Render { blob, params, ancho } |
Miniatura JPEG de un blob guardado, procesada con las ediciones del mazo | urlMiniatura(blob, PreviewParams, ancho) |
/t/<sha256 of URL> |
Miniatura { huella } |
Imagen de un resultado de búsqueda (PNG o JPEG, detectado por los bytes mágicos) | urlBusqueda(huella) |
/d/<huella>/<width> |
Diseno { huella, ancho } |
Vista previa PNG de un diseño registrado con design_register
|
urlDiseno |
/c/<huella>/<width> |
Capa { huella, ancho } |
PNG de la carta sin su ilustración (design_overlay) |
urlCapa |
/b/<sha256>/<side> |
Arte { blob, lado } |
Ilustración del almacén de blobs, reducida para que su lado largo sea de 1000 o 2000 px (JPEG, o PNG si tiene transparencia) | urlArte(blob, 1000 | 2000) |
/x/<pack>/<n> |
Extra { pack, indice } |
Imagen PNG extra de un pack de marcos descargable | urlExtra(pack, n) |
/f/<sha256>.woff2 |
Letra { huella } |
Fichero de letra de un tema de tweakcn instalado |
raizProtocolo() usado por src/lib/temas-instalados.ts
|
/m/<sha256>/<ancho> |
Mesa { imagen: ImagenMesa::Propia, ancho } |
JPEG de una cara de la mesa de juego preparada por la app (o de un tapete) | urlMesa({ tipo: "propia" }, ancho) |
/m/s/<id de Scryfall>/<cara>/<ancho> |
Mesa { imagen: ImagenMesa::Scryfall, ancho } |
JPEG de una impresión de Scryfall sin retocar usada en la mesa | urlMesa({ tipo: "scryfall" }, ancho) |
Validación, aplicada y probada con tests unitarios (los tests de protocol.rs las_rutas_raras_se_rechazan y otros):
-
<sha256>/<huella>deben pasarstore::blobs::es_huella(exactamente 64 caracteres hexadecimales en minúscula), lo que además bloquea el path traversal. -
<width>debe ser uno deANCHOS = [146, 244, 488, 672, 745, 1000](reflejado porANCHOSenimage-url.ts);<side>uno deLADOS_ARTE = [1000, 2000](diseno/vista.rs). -
<params>es el base64url (sin relleno) del JSON dePreviewParams(render.rs):edits,edge(oscurecido de bordes),back,bleedMm,bleedMode,fillCorners,card(CardSize) y elselloopcional (DesignLang). Máximo 2048 caracteres (MAX_PARAMS); debe decodificarse y deserializarse. -
<pack>son 1–40 caracteres de[A-Za-z0-9_-];<n>un entero no negativo. - Los anchos de
/m/tienen que ser deANCHOS_MESA(los seis deANCHOSmás 1920, para el tapete);<id de Scryfall>tiene que pasarmesa::imagenes::es_id_scryfall(36 caracteres, dígitos hexadecimales y guiones) y<cara>es 0 o 1 (testlas_rutas_de_la_mesa). Los ficheros salen deapp_cache_dir/mesa/; con 745 o más se sirven tal cual y los anchos menores se reducen una vez enrenders/m/(mesa/imagenes.rs::servir). Véase Mesa de juego. - Códigos de estado: una forma reconocida pero no parseable es
400, una forma desconocida404, un blob/diseño que falta404, otro fallo de renderizado500, una descarga remota fallida para/t/502. - Toda respuesta pone
X-Content-Type-Options: nosniffyAccess-Control-Allow-Origin: *; las correctas añadenCache-Control: max-age=31536000, immutable, porque la URL contiene todo lo que cambia la imagen.
Por qué URLs en lugar de bytes por IPC: ningún byte de imagen cruza el IPC y WebKit cachea por URL. La URL lleva todos los parámetros de renderizado, así que cambiar una edición o un ajuste simplemente produce una URL nueva.
Las rutas /r/, /d/, /c/, /b/ primero hacen await estado.renders.acquire() (semáforo dimensionado al número de núcleos) y luego ejecutan el trabajo de CPU en tauri::async_runtime::spawn_blocking. /x/ y /f/ son lecturas pequeñas de fichero y se saltan el semáforo. /m/ corre en spawn_blocking sin el semáforo: las imágenes de la mesa se preparan antes, así una petición es leer un fichero o, una vez por ancho, reducirlo. Como el frontend solo monta <img> para las páginas visibles (useVisible, véase Vista previa en vivo), la cola del semáforo se mantiene corta.
- Busca
app_cache_dir/renders/t/<hash>.jpg; lo sirve si existe. - Si no, busca el hash en
AppState.miniaturas; un hash desconocido es404. - Descarga: las URLs que empiezan por
https://cards.scryfall.io/usanscryfall.download; cualquier otra pasa porexterno.bajar(url, MAX_IMAGEN)yfuentes::limpiar_imagen(decodificar y recodificar), de modo que solo se obtienen fuentes de la lista permitida. - Escribe el fichero de forma atómica en la caché y lo devuelve.
Así, el WebView nunca contacta con ningún host de imágenes; la CSP de img-src solo permite 'self' pbimg: http://pbimg.localhost data: blob:.
src-tauri/src/render.rs convierte un blob más unos ajustes en píxeles, tanto para la pantalla como para el PDF.
| Elemento | Comportamiento |
|---|---|
PreviewParams (#[derive(TS)]) |
Lo que la pantalla pone en la URL. Rust decide si el oscurecido de bordes se aplica a esta imagen a partir de su origen y de si es un reverso (proxy_core::imaging::pipeline::edge_for), la misma regla que al exportar. sello (opcional, #[ts(optional)]): pintar el sello de Tapete en este idioma; solo lo manda la vista previa de las hojas (Slot.sello), nunca el editor, las listas ni la mesa (El sello de las cartas). |
spec_de_miniatura(blob, img, params, ancho) |
RenderSpec con target: SpecTarget::Width(ancho), calidad JPEG 85, sello de los parámetros. |
spec_de_casilla(slot, img, edits, settings) |
RenderSpec para una casilla del PagePlan a resolución de impresión: el tamaño de carta es el recorte de la casilla (así funcionan las cartas grandes), bleed_mm es el bleed_full_mm de la casilla, SpecTarget::Dpi(quality.dpi()), codificación según los ajustes de impresión; las ediciones solo se aplican si slot.apply_edits; sello: None (en el PDF el sello es texto vectorial encima, así que el JPEG puede pasar tal cual). |
AppState::carpeta_renders() |
paths.cache/renders. |
AppState::leer_blob(hash) |
Lee el fichero localizado por el almacén de blobs, sondea el tamaño, lee origin de la tabla blobs; NotFound (parámetro blob) si falta. |
AppState::miniatura(blob, params, ancho) |
Fichero de caché renders/<RenderSpec::key()>.jpg; si no está, renderiza y luego lo escribir_atomico; los errores al escribir la caché se ignoran. |
render_para_pdf(state, spec) |
Renderiza para imprimir sin caché en disco («pesa demasiado»). |
From<ImagingError> for AppError |
Véase IPC y manejo de errores. |
RenderSpec::key() es el SHA-256 de (PIPELINE_VERSION, spec) serializado como JSON (proxy-core/src/imaging/pipeline.rs, PIPELINE_VERSION = 5). Cualquier cambio en el spec o un aumento de la versión de la canalización da un nombre de fichero distinto, lo que invalida los ficheros de caché antiguos sin borrarlos (los elimina cache_clear). Las claves de la caché PNG del diseñador incluyen su propia versión del motor (proxy_core::diseno::VERSION_MOTOR).
Organización de la caché bajo app_cache_dir/renders/:
| Ruta | Contenido |
|---|---|
<spec key>.jpg |
Miniaturas de blobs |
t/<url hash>.jpg |
Imágenes de resultados de búsqueda (pueden ser bytes PNG pese a la extensión; el protocolo detecta el tipo) |
d/<huella>-<width>.png |
Vistas previas en vivo de diseños |
c/<huella>-<width>.png, c/<huella>.json
|
Capa del diseño y la información de su ventana de ilustración |
b/<blob>-<side>.jpg o .png
|
Ilustración reducida |
cache_stats y cache_clear (commands/library.rs) miden y hacen remove_dir_all exactamente de esta carpeta. Regla: aquí solo viven derivados; las imágenes de los mazos nunca están en este árbol. Un test unitario de commands/library.rs comprueba que la carpeta de renders está bajo paths.cache y no bajo paths.data.
El escritor atómico genérico es store/blobs.rs → escribir_atomico(destino, bytes): crea los directorios padre, escribe <name>.<ext>.part, hace fsync y renombra. Un fallo nunca deja un fichero a medio escribir con su nombre definitivo. Los PDF, los ficheros de paquete, las cachés y los blobs lo usan todos.
| Clave | Valor / significado |
|---|---|
productName, identifier, version
|
Tapete (el nombre visible; Proxy Builder antes de la 4.0.0), un identificador de paquete en DNS inverso que nunca cambia (léelo en el fichero; de él salen las carpetas de datos, el id de Android y las claves del registro de Windows, véase Versiones y actualizaciones automáticas), la versión actual (se mantiene a la par con Cargo.toml y package.json; véase Versiones y actualizaciones automáticas) |
build |
beforeDevCommand: pnpm dev, devUrl: http://localhost:1520 (tiene que coincidir con el puerto de Vite de vite.config.ts), beforeBuildCommand: pnpm build, frontendDist: ../dist
|
app.windows[0] |
label main, 1440x900, mínimo 960x640, visible: false, backgroundColor: #1d1b19, create: false (la construye crear_ventana) |
tauri.android.conf.json |
Un parche de fusión JSON que solo se aplica en las compilaciones de Android. Un parche de fusión reemplaza las listas enteras, así que repite app.windows completo (la misma ventana sin el tamaño mínimo de escritorio) y pone bundle.android.minSdkVersion a 26. |
plugins.deep-link |
desktop.schemes y, para móviles, mobile con los esquemas proxybuilder y tapete (appLink: false: esquemas propios, sin enlaces web verificados; los dos tienen que coincidir con enlace::ESQUEMAS, lo comprueba un test). En Android el intent-filter correspondiente está en el AndroidManifest.xml. |
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 es null (el HMR de Vite funciona en desarrollo). |
bundle |
targets: all, createUpdaterArtifacts: true, categoría Utility, shortDescription; el paquete Debian tiene depends: [] (aquí no se fuerza WebKitGTK como dependencia) y conflicts/replaces/provides: proxy-builder; el RPM, conflicts/obsoletes: proxy-builder; windows.nsis.installerHooks: ./windows/hooks.nsh. Estos tres últimos reemplazan una instalación hecha con el nombre viejo (véase Versiones y actualizaciones automáticas) |
plugins.updater |
Clave pública de Minisign, dos endpoints en orden, https://github.com/PoolFlamingo/tapete/releases/latest/download/latest.json y después https://github.com/PoolFlamingo/proxy-builder/releases/latest/download/latest.json (véase Versiones y actualizaciones automáticas), requireSignedVersion: true, installMode: passive en Windows |
Consecuencias de la CSP: el WebView no puede hacer fetch de nada fuera del IPC, no puede cargar scripts ni imágenes remotos y no puede enviar formularios. 'wasm-unsafe-eval' deja al WebView compilar WebAssembly (la física de los dados 3D, véase Mesa de juego); no permite eval de JavaScript. style-src 'unsafe-inline' hace falta por los estilos en línea de React y el <style> inyectado para los temas instalados. Las letras de los temas instalados vienen de pbimg://…/f/…. El test la_configuracion_del_actualizador_es_valida de src-tauri/src/actualizar.rs lee tauri.conf.json y falla si cambian la firma del actualizador, la forma del endpoint o la cabecera de la clave pública.
Tres ficheros, todos para la ventana main. default.json (todas las plataformas) concede únicamente: core:default, core:window:allow-show, core:window:allow-destroy, core:window:allow-set-fullscreen, core:window:allow-minimize, core:window:allow-close, opener:default, dialog:default. escritorio.json (platforms: linux, macOS, windows) añade window-state:default: salió de default.json porque ese permiso no existe en Android, donde el plugin no está, y la compilación valida cada identificador. movil.json (android, iOS) añade barcode-scanner:default, para la cámara que lee el QR de «Recibir un deck» (src/lib/escaner.ts, la única entrada a ese plugin). La descripción enuncia la regla: ningún permiso fs; todo el acceso a disco pasa por comandos de Rust. core:window:allow-show deja que main.tsx muestre la ventana; core:window:allow-destroy deja que la guarda de cierre (ConfirmarCierre, Ajustes y shell) la cierre: con un escuchante de JavaScript en onCloseRequested, Tauri deja de cerrar la ventana por su cuenta y lo hace destroy(), y ese permiso no está incluido en core:default ni en core:window:default, así que sin la entrada explícita la ventana no se podría cerrar (la compilación valida el identificador). core:window:allow-set-fullscreen es para F11 y los botones de pantalla completa (src/lib/ventana.ts, Ajustes y shell); core:window:allow-minimize y core:window:allow-close son para los controles de la ventana que la barra de arriba enseña a pantalla completa (close() pasa por la guarda de cierre, no por destroy()). El plugin del actualizador no tiene entrada de capability porque solo se usa desde Rust (actualizar.rs, commands/actualizar.rs). Los comandos propios registrados con generate_handler! no están limitados por una lista de permisos por comando en esta configuración (la app no define un conjunto de permisos propio; Deducido de que el fichero de capabilities solo contiene permisos de plugins y del core).
| Plugin (crate) | Usado desde | Para |
|---|---|---|
tauri-plugin-dialog |
JS (src/lib/dialogos.ts) |
Diálogos de abrir/guardar/carpeta (en Android devuelven un content://, véase IPC y manejo de errores) |
tauri-plugin-fs |
Solo Rust (fichero.rs) |
Abre un content:// y da un File de verdad; al WebView no se le concede ningún permiso fs:
|
tauri-plugin-android-extra (propio, plugins/android-extra/, solo móvil) |
Solo Rust (android.rs) |
Compartir, instalar un APK, pantalla encendida y completa, bloqueo de multicast de la Wi-Fi, nombre de un content://, ficheros abiertos desde otra app (véase abajo) |
tauri-plugin-barcode-scanner (móvil) |
JS (src/lib/escaner.ts) |
La cámara que lee un QR |
tauri-plugin-opener |
Rust (OpenerExt en commands/export.rs, commands/temas.rs) |
Abrir ficheros exportados, mostrarlos en su carpeta, abrir la página de la comunidad de tweakcn |
tauri-plugin-updater |
Solo Rust (actualizar.rs) |
Comprobar, descargar, verificar la firma, instalar |
tauri-plugin-deep-link |
Solo Rust (enlace.rs, montado en lib.rs) |
Enlaces de invitación proxybuilder:// y tapete://, también en Android (sección mobile de la configuración); sin entrada en las capacidades porque la pantalla nunca lo llama |
tauri-plugin-single-instance (escritorio, feature deep-link) |
Solo Rust (lib.rs), no con PB_PERFIL
|
Un segundo arranque pasa sus argumentos (el enlace) a la app abierta |
tauri-plugin-window-state (escritorio) |
Plugin + save_window_state de Rust |
Recordar tamaño, posición, maximizada; nunca la visibilidad |
Otras dependencias de plataforma: gtk en Linux y arboard en el resto, ambas para leer la imagen del portapapeles desde Rust (diseno/portapapeles.rs; WebKitGTK rechaza navigator.clipboard.read() en el webview).
-
[profile.dev] debug = "line-tables-only",split-debuginfo = "packed", dependencias condebug = false: mantienetarget/pequeño. -
[profile.dev.package."*"] opt-level = 2: las dependencias (image, resize, PDF) se compilan optimizadas incluso en desarrollo, porque el procesado de imagen sin optimizar vuelve inutilizable la vista previa bajopnpm tauri dev. -
reqwestusarustlscondefault-features = false; el CI falla si aparece OpenSSL encargo tree. - Dependencias según el destino: single-instance y window-state solo fuera de Android e iOS;
gtken Linux yarboarden Windows y macOS (imagen del portapapeles: un móvil no la tiene);webpki-root-certs,webpki-rootsybarcode-scannersolo en móvil. -
actualizar-locales la única feature de Cargo, no hay ninguna por defecto y nunca se activa en el CI ni en una release (un test deactualizar.rsleeCargo.toml, los dos workflows ypackage.jsonpara asegurarlo): véase Versiones y actualizaciones automáticas. - Dependencias de la mesa de juego:
getrandom(la semilla de cada partida, de la entropía del sistema),regex(el «+N/+N» fijo de auras y equipos) yflate2con el motorzlib-rs(la partida guardada; el mismo zlib-rs que ya traezip).
| Módulo | Responsabilidad | Página con más detalle |
|---|---|---|
commands/* |
Comandos de Tauri, un fichero por área | IPC y manejo de errores |
error.rs |
AppError, ErrorCode, implementaciones de From
|
IPC y manejo de errores |
state.rs, jobs.rs, registro.rs
|
Estado compartido, flags de trabajos, fichero de registro | esta página |
protocol.rs, render.rs
|
pbimg:// y renders |
esta página, Vista previa en vivo |
store/ (db, blobs, decks, presets, settings, colecciones, etiquetas, listas, partidas, linaje, migrations/) |
Biblioteca SQLite y almacén de blobs | Almacenamiento y blobs |
package.rs |
Paquetes ZIP .pbdeck
|
Formato del paquete de mazo |
optimizar.rs |
Optimizar las imágenes de los decks en el propio equipo | Almacenamiento y blobs |
biblioteca.rs |
Detalles por mazo para la biblioteca (colores, clase, precio, idiomas), en caché por updated_at
|
Biblioteca de mazos |
scryfall/ (client, rate, cache, models, resolve, busquedas, fichas, fichas_auto, nombres) |
Acceso a Scryfall, catálogo de nombres, resolución de listas | Integración con Scryfall |
archidekt.rs |
Importación desde una URL de Archidekt (ids exactos de Scryfall) | Integración con Scryfall |
fuentes/ (cockatrice, mtgio, mtgjson, mod.rs con Externo) |
Fuentes que no son Scryfall tras una lista permitida | Fuentes externas |
ocr.rs, originales.rs
|
OCR opcional y detección del nombre original | Identificación de cartas y OCR |
diseno/ |
Parte de la app del diseñador de cartas (borradores, renderizado con recursos, vista previa en vivo, ilustración, exportación a PNG, lote, descarga de packs y letras, portapapeles) | Capa de aplicación del diseñador |
temas.rs |
Temas y letras de tweakcn | Temas y skins |
iconos.rs |
Iconos de Iconify del selector de iconos (catálogo en caché en app_cache_dir/iconos/, elegidos en app_data_dir/iconos/elegidos.json, limpieza del SVG) |
Iconos |
actualizar.rs |
Envoltorio del actualizador (instaladores de escritorio y el APK de Android) | Versiones y actualizaciones automáticas |
tls.rs |
El TLS de cada plataforma, el único sitio que crea un cliente HTTP o un verificador | esta página, «Android y la capa móvil» |
ciclo.rs |
Ciclo de vida de Android: guardar al irse, reconectar al volver | esta página |
android.rs y plugins/android-extra/
|
El contrato con el plugin de Kotlin: compartir, instalar un APK, pantalla, multicast, nombres, ficheros abiertos desde fuera | esta página |
fichero.rs, abrir.rs, commands/fichero.rs
|
Ficheros elegidos como ruta o content://; abrir un .pbdeck desde otra app; compartir |
IPC y manejo de errores, Exportación |
pasar/ (encontrar, recibir, enviar, imagenes, protocolo, tipos), store/linaje.rs y commands/pasar.rs
|
Pasar decks entre equipos por el nodo de iroh compartido; la tabla del linaje de los decks | Pasar decks |
enlace.rs |
Enlaces: acepta tapete:// y proxybuilder:// con unirse/<código PB1> o amigo/<código TAP>; monta los enlaces de la web que se comparten (enlace_invitacion → /join/<código> sin IP, enlace_amigo → /friend/<código>); enlace_pendiente, evento enlace://entrante
|
Juego en línea, Amigos y mensajes |
amigos/ (firma, cifrado, cliente, sincronizar, avisar), store/amigos.rs y commands/amigos.rs
|
Amigos y mensajes: peticiones firmadas al servidor de Tapete, la clave de cifrado derivada de la identidad, mensajes cifrados de extremo a extremo, el bucle de sincronización, los avisos | Amigos y mensajes |
mesa/ (sesion, preparar, imagenes, guardada, tapete, ejemplo) |
La sesión de la mesa de juego alrededor del crate mesa, la preparación de decks e imágenes, la partida guardada y el tapete |
Mesa de juego |
red/ (perfil, identidad, nodo, protocolo, codigo, anfitrion, invitado, mirada, imagenes, latencia, mesa, colores, sucesion, vuelta) y commands/red.rs
|
El juego en red: perfiles de debug, identidad, nodo de iroh, protocolo de la partida, código directo, host e invitado, espectadores, imágenes por la red, el traspaso del anfitrión sin servidor, volver a una partida | Juego en línea |
El mismo crate compila para Android (pnpm tauri android build, proyecto en src-tauri/gen/android/, que está versionado: minSdk 26, paquete com.estigio.proxybuilder, intent-filter para proxybuilder://, tapete:// y los ficheros .pbdeck, un FileProvider sobre la caché). Hay un solo crate de la app y un solo código Rust; lo que cambia va tras cfg(mobile) / cfg(target_os = "android") y en los ficheros de abajo. El comportamiento de escritorio no debe cambiar (el ciclo de vida, el TLS y los ganchos de Kotlin no hacen nada allí). Véase Móvil para el conjunto, incluido el lado de la pantalla.
flowchart LR
S["pantalla (React)"] -- "call()" --> C["commands/*"]
C --> F["fichero.rs: ruta o content://"]
C --> A["android.rs"]
A -- "register_android_plugin" --> K["plugins/android-extra (Kotlin)"]
K -- "Channel: fichero abierto desde fuera" --> AB["abrir.rs"]
W["WindowEvent::Suspended / Resumed"] --> CI["ciclo.rs"]
CI -- "ciclo://pausa, ciclo://vuelta" --> S
CI --> M["guardar la partida / network_change()"]
T["tls.rs"] --> H["clientes HTTP (actualizador, servidor de amigos…), iroh"]
Hay dos consumidores de TLS: los clientes HTTP (reqwest: Scryfall, el actualizador, el servidor de amigos de Tapete…) y el nodo de iroh. src-tauri/src/tls.rs da a cada uno su configuración (reqwest(builder), iroh()), y un test (nadie_crea_un_cliente_ni_un_verificador_fuera_de_tls_rs) falla si cualquier otro fichero construye a mano un cliente o un verificador.
-
Escritorio: el verificador del sistema (
rustls-platform-verifier,CaTlsConfig::system()): los certificados en los que el sistema ya confía, con su revocación. -
Android (e iOS, hasta que se pruebe allí): los certificados raíz de Mozilla incrustados en la app (
webpki-root-certsparareqwest,webpki-rootspararustls,CaTlsConfig::embedded()para iroh).
Por qué: el verificador de la plataforma entra en pánico en Android si no se inicializa con su propia parte de Kotlin, y inicializarlo exige un contexto JNI al arrancar. Las raíces incrustadas no necesitan ni Kotlin ni JNI en el arranque. El precio es que una autoridad que alguien haya instalado a mano en el teléfono no es de fiar, cosa que esta app no necesita (solo habla con Scryfall, GitHub, las fuentes de Fuentes externas, el servidor de amigos de Tapete y los relés de iroh). El actualizador recibe su cliente con UpdaterBuilder::configure_client(tls::reqwest).
tls.rs guarda también AGENTE, el único User-Agent de las peticiones HTTP de la app: Tapete/<version> (+https://tapete.zodhub.app). Todo cliente de reqwest lo pone (.user_agent(tls::AGENTE)): Scryfall, commands/cards.rs (Archidekt), fuentes::Externo, los modelos del OCR, las letras libres y las CJK y los marcos Original. La excepción es el cliente del actualizador: configure_client(tls::reqwest) solo le pone el TLS (Deducido: el plugin mantiene su propio user agent). Por qué una sola constante: Scryfall pide a cada cliente un nombre y una forma de contacto, y GitHub, Google Fonts y los demás sitios ven el mismo nombre, así que el nombre y la versión no pueden desfasarse entre clientes. Los tests usan su propio Tapete-test.
Un móvil no avisa de que va a congelar o matar el proceso (en Android 14 y posteriores una app en caché se congela unos diez segundos después de salir de la pantalla y se cierran sus sockets TCP), así que guardar no puede esperar a un cierre que no llega. ciclo::evento es el on_window_event de la app:
-
WindowEvent::Suspended: guarda la partida abierta si hay algo sin guardar (commands::mesa::guardar_si_toca) y emiteciclo://pausa; la pantalla (src/lib/ciclo-de-vida.ts) guarda el deck y la carta que se diseña. -
WindowEvent::Resumed: emiteciclo://vueltay avisa al nodo de iroh de que la red puede haber cambiado (red.cambio_de_red(), es decir,Endpoint::network_change()), porque Android no avisa al código nativo de los cambios de red. Una partida en red reconecta como tras un corte. - Nunca se llama a
iroh::dns::install_android_jni_context:taoya inicializa el contexto de Android y hacerlo dos veces entra en pánico.
plugins/android-extra/ es el plugin de Tauri propio del proyecto (miembro del workspace; su primer Kotlin). AndroidExtraPlugin.kt hace lo que Tauri y sus plugins oficiales no hacen: compartir (ACTION_SEND de un fichero de la caché por el FileProvider, tras el selector del sistema), pantallaEncendida (FLAG_KEEP_SCREEN_ON), pantallaCompleta (esconde las barras del sistema; un gesto desde el borde las enseña un momento), multicast (un MulticastLock de la Wi-Fi: sin él Android tira los paquetes mDNS que encuentran los equipos cerca; se suelta al destruir la actividad; el plugin tiene un solo lock sin contador, así que android::multicast(app, uso, si) cuenta en Rust quién lo usa, UsoMulticast::Buscar y Recibir, y solo lo coge con el primero y lo suelta con el último), instalarApk, nombreDeUri (el DISPLAY_NAME de un content://, que lleva la extensión que la URI puede no tener) y escucharAbiertos (los ficheros que otra app abre con esta, ACTION_VIEW, entregados por un Channel; el que llega antes de que Rust escuche se guarda y se entrega cuando lo hace; el intent de arranque se ignora si lleva FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY, porque volver desde Recientes tras morir el proceso entrega otra vez el intent original y ese fichero ya se importó).
La regla: solo Rust llama al plugin, por src-tauri/src/android.rs, una función por comando. El plugin no declara permisos para el WebView, así que la pantalla no puede alcanzarlo; ella pregunta «¿es un móvil?» con src/lib/plataforma.ts y hace todo lo demás por los comandos de IPC y manejo de errores. Fuera de Android cada función no hace nada y devuelve Ok (pantalla encendida, multicast, escuchar ficheros abiertos, nada que retocar) o devuelve Unsupported (detalle: android) donde no tiene sentido sin Android (compartir, instalar, pantalla completa). Un comando que llama a una función del segundo tipo debe decidir si Unsupported es un fallo; app_pantalla_completa lo toma como «nada que hacer».
instalar_apk tiene un resultado más: si el sistema no deja a la app instalar paquetes (canRequestPackageInstalls() es falso) el plugin abre el ajuste de «instalar apps desconocidas» de esta app y contesta lanzado: false; Rust lo convierte en Unsupported con motivo: instalar_permiso, y la persona vuelve y pulsa Instalar otra vez (Versiones y actualizaciones automáticas).
fichero.rs (rutas y content://), commands/fichero.rs (fichero_temporal, compartir_fichero, que solo comparte un content:// o un fichero dentro de app_cache_dir y si no falla con InvalidInput, motivo: compartir_fuera, para que la vista web no pueda hacer compartir la base de datos ni la clave del nodo) y abrir.rs (un .pbdeck abierto desde otra app: pbdeck://abierto, pbdeck_pendiente) se describen en IPC y manejo de errores. abrir.rs solo se arranca en Android (abrir::escuchar en setup). Dos intent-filter del manifiesto traen ficheros hasta aquí: uno para URI content/file cuyo nombre acaba en .pbdeck (varios tipos MIME, porque Android da a un .pbdeck cualquiera de zip, x-zip-compressed u octet-stream; cuatro pathPattern con cero a tres puntos más, porque el comparador de Android no reintenta y mi.deck.pbdeck no casaría con uno solo) y un segundo, limitado al proveedor de Descargas, porque las URI que reparte (.../document/msf:1234) no tienen nombre con el que casar. Efecto secundario del segundo: la app se ofrece para cualquier zip o binario abierto desde Descargas, y Rust rechaza lo que no sea un paquete al leerlo (package::import no se fía de su entrada).
| Qué | Escritorio | Android |
|---|---|---|
| Estado de la ventana |
tauri-plugin-window-state (+ capability escritorio) |
ninguno |
Instancia única, registro de proxybuilder:// y tapete:// en ejecución |
sí | lo hacen el sistema y el manifiesto |
| Actualizador | el instalador de la plataforma | el APK (abajo, y Versiones y actualizaciones automáticas) |
| Imagen del portapapeles |
gtk / arboard
|
ninguna |
| Diálogos | rutas | content:// |
| TLS | verificador del sistema | raíces incrustadas |
Deducido: iOS está preparado (Plataforma::Ios, dependencias solo de móvil bajo cfg(any(android, ios))) pero no se ha compilado nada para él; véase Móvil.
-
Nunca bloquees el runtime asíncrono con trabajo de CPU. Usa
spawn_blocking(como hacenexport_pdf, los manejadores del protocolo yen_hiloencommands/diseno.rs). -
Mantén
state.trabajo()en cada trabajo largo oblob_gcyupdate_installpodrían ejecutarse bajo él. Una partida abierta cuenta como ocupado por sí sola (hay_partida()). -
cache_clearsolo debe tocarrenders/. Una nueva ubicación de caché para datos críticos del mazo rompería esta garantía; ponlos enapp_data_diry en el almacén de blobs. -
Añade las rutas nuevas del protocolo con un test en
protocol.rstanto de aceptación como de rechazo (el path traversal, las longitudes de hash incorrectas, el hexadecimal en mayúsculas y los anchos incorrectos ya están cubiertos). -
No edites migraciones ya publicadas; añade el número siguiente (
0010_*.sql) y añádelo aMIGRACIONESenstore/db.rs. -
Almacén de blobs frente a caché: el almacén de blobs está en
app_data_dirporque los mazos dependen de él; la GC conserva las imágenes de menos de 24 h (GRACIA_MS) y todo lo referenciado pordeck_cards,decks.default_back_blob,blob_refso el ajuste del reverso genérico. -
Las compilaciones release ocultan la consola en Windows; usa el fichero de registro en
app_log_dirpara depurar el comportamiento de red.
| Dónde | Qué |
|---|---|
src-tauri/src/protocol.rs (mod tests) |
Las rutas válidas se parsean; se rechazan las malformadas, el traversal, los anchos/lados incorrectos, los nombres de pack incorrectos y .ttf en lugar de .woff2; las rutas de la mesa de juego (las_rutas_de_la_mesa) |
src-tauri/src/mesa/ (sesion.rs, guardada.rs, preparar.rs) |
Los parches dan la vista entera, los rechazos llevan su motivo, reiniciar, una partida abierta deja la app ocupada, guardar y seguir conserva la huella, la segunda preparación no pinta nada |
src-tauri/src/render.rs (mod tests) |
Construcción de RenderSpec a partir de casillas, conversión de errores |
src-tauri/src/jobs.rs |
alta_cancelar_y_baja |
src-tauri/src/error.rs |
Serialización de errores |
src-tauri/src/actualizar.rs |
La configuración del actualizador en tauri.conf.json es válida |
src-tauri/src/commands/library.rs |
La carpeta de la caché está bajo la caché, no bajo los datos |
src-tauri/src/commands/carpetas.rs |
la_carpeta_de_una_ruta |
src-tauri/src/tls.rs |
nadie_crea_un_cliente_ni_un_verificador_fuera_de_tls_rs: ningún fichero salvo tls.rs contiene Client::builder() (si no va envuelto en tls::reqwest(), with_platform_verifier ni CaTlsConfig::
|
src-tauri/src/fichero.rs, abrir.rs, commands/sistema.rs
|
El manejo de content:// contra un Acceso simulado; solo cuentan como ficheros abiertos los content:// de tamaño razonable y uno pendiente se entrega una sola vez; la pantalla completa sin Android no es un fallo |
- Comando o trabajo nuevo: IPC y manejo de errores.
- Recurso compartido nuevo: añade un campo a
AppStatee inicialízalo enAppState::open; haz que los tests lo construyan mediantePathssobre directorios temporales. - Algo que solo Android puede hacer: una función
@Commandenplugins/android-extra/android/src/main/java/.../AndroidExtraPlugin.kt, su envoltorio ensrc-tauri/plugins/android-extra/src/movil.rsy una función enandroid.rscon su gemela fuera de Android (Unsupporteddonde no tiene sentido sin Android,Okdonde solo retoca la ventana). Nunca se llama al plugin desde la pantalla. - Plugin nuevo: añade el crate,
.plugin(...)enlib.rs, un permiso encapabilities/default.jsonsolo si lo usa el WebView, y actualiza la CSP solo si necesita orígenes nuevos (evítalo; es preferible un comando de Rust).
- Arquitectura
- IPC y manejo de errores
- Almacenamiento y blobs
- Vista previa en vivo
- Imagen y PDF
- Capa de aplicación del diseñador
- Mesa de juego
- Juego en línea
- Amigos y mensajes
- Versiones y actualizaciones automáticas
- Preparación del entorno de desarrollo
- Móvil
- Pasar decks
- Exportación
- Fuentes externas
- El sello de las cartas
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
- Rename-to-Tapete · Cambio de nombre a Tapete
- License · Licencia
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
- The-Card-Stamp · El sello de las cartas
- 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
- Game-Engine · Motor de la mesa
- Game-Table · Mesa de juego
- Online-Play · Juego en línea
- Friends-and-Messages · Amigos y mensajes
Mobile · Móvil
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
- Card-Search · Búsqueda de cartas
- Set-Browser · Explorador de sets
Reference · Referencia