Repository navigation
Ajustes y shell
English · Español
El shell es el marco de la app: la barra superior (selector de vista, nombre del mazo, indicador de guardado, botón de actualización, botón de ayuda, botón de Ajustes), los tres paneles redimensionables de la vista de mazo y los hooks de arranque que abren el último mazo y arrancan el autoguardado. El diálogo de Ajustes contiene el idioma de la interfaz, las actualizaciones dentro de la app, el índice opcional de nombres multilingües y todo el sistema de apariencia. Esta página documenta ambos, qué hace cada ajuste y dónde se persiste.
Código: src/features/shell/ y src/features/settings/.
| Fichero | Responsabilidad |
|---|---|
app-shell.tsx |
AppShell: cabecera + área principal; NombreDeck (input en línea del nombre del mazo); monta <Tutorial /> y llama a useTutorialInicial() (Tutoriales); monta <ConfirmarCierre />; cambiarVista pregunta antes de salir de Diseñar con la carta sin guardar (más abajo); useEnlacesEntrantes (src/features/amigos/enlaces.ts) pasa a Jugar cuando llega y se acepta un enlace de partida tapete://unirse/<código> (o proxybuilder://unirse/<código>) (con una partida en marcha se rechaza con un toast; Juego en línea), y abre «Amigos» con el código puesto si es un enlace tapete://amigo/<código> (Amigos y mensajes). |
scroll-por-vista.ts |
useScrollPorVista(vista, raiz): recuerda el scroll de cada vista y lo devuelve al volver a ella (más abajo). |
confirmar-cierre.tsx |
ConfirmarCierre: la guarda que pregunta antes de cerrar la ventana cuando se perdería algo (más abajo). |
use-deck-inicial.ts |
useDeckInicial(): arranque del mazo, el autoguardado, el plan y las teclas de deshacer/rehacer. |
save-indicator.tsx |
SaveIndicator: «Guardado / Guardando… / Sin guardar / Error al guardar» (la última es un botón que reintenta). compacto (móvil): solo el icono; el texto queda para el lector de pantalla. |
actualizacion-automatica.tsx |
Bucle de actualización automática, toasts de actualización, BotonActualizar, PuntoActualizacion, useAvisoAjustes. |
barra-movil.tsx |
BarraInferiorMovil (la barra de pestañas de abajo del móvil) y HojaMas (la hoja «Más», una HojaMenu de FilaMenu; el puntito de la actualización es la marca de la fila de Ajustes): véase «El shell según el formato». |
atras-shell.ts |
Las acciones de «atrás» de Android del shell: destinoDeAtras (desde Diseñar, «atrás» va a Deck), atrasPorDefecto y crearSalidaConDobleToque («Pulsa otra vez para salir»). |
mesa-completa.ts |
useMesaAPantallaCompleta() (una partida abierta en el móvil esconde las barras del shell) y salirDeLaMesa() (vuelve a la pantalla de la que se vino; la partida sigue abierta). |
controles-ventana.tsx |
ControlesVentana: minimizar, salir de pantalla completa y cerrar, al final de la barra de arriba, solo mientras la ventana está a pantalla completa (más abajo). |
De izquierda a derecha:
- Logo y título:
<Logo>(src/components/logo.tsx: la mano que juega una carta,game-icons:card-playde Iconify, sobre el cuadrado redondeado verde del paño, el mismo que el icono de la app y el de la web; sus colores son los de la marca y no siguen el tema, así que se ve igual con cualquier tema; el icono es CC BY 3.0, así que la cabecera de Ajustes lo atribuye bajo la descripción: «Logo e iconos de juego: game-icons.net (CC BY 3.0)», que cubre también los demás iconosgame-icons:*de la interfaz, igual que los README) y «Tapete» (el nombre visible desde la 4.0.0), subtítulo «Scryfall · listo para imprimir»; el subtítulo se oculta por debajo del puntolgpara que una ventana estrecha deje sitio al nombre del deck). - Un
ToggleGroupde selección única ligado auseUi().vista: Deck, Diseñar, Biblioteca, Buscar (véase Búsqueda de cartas), Jugar (iconolucide:swords, la mesa de juego).onValueChangesolo acepta estos cinco valores (pulsar el elemento activo devuelve un valor vacío que se ignora, así que siempre hay uno seleccionado). -
NombreDeck(oculto en las vistas Biblioteca y Jugar): unInputque editadeck.name. Confirma al perder el foco o con Enter, Escape revierte; un valor vacío revierte; los cambios pasan poruseDeck.setSinHistorial({ name })(se guarda, no se puede deshacer). Mide 16 rem pero puede encogerse hasta 6 rem (min-w-24 shrink) en una ventana estrecha. - A la derecha:
SaveIndicator,BotonActualizar, el botón de ayuda (botón de iconolucide:circle-help, con tooltip yaria-label«Tutorial de esta pantalla»; llama auseUi().abrirTutorial(vista)con la pantalla actual, véase Tutoriales) y el botón de Ajustes (por debajo delg, solo su icono; el tooltip dice entonces «Ajustes», salvo que lleve el aviso de actualización). - Al final, solo mientras la ventana está a pantalla completa:
ControlesVentana(minimizar, salir de pantalla completa, cerrar). Véase «Pantalla completa (F11) y controles de la ventana».
Varios de estos elementos llevan un atributo data-tutorial (pestanas, nombre-deck, ayuda, ajustes): son las anclas que rodea el recorrido del deck.
SaveIndicator lee useDeck().guardado / errorGuardado; no renderiza nada hasta que hay un mazo abierto. Con error, la etiqueta pasa a ser un botón pequeño que vuelve a llamar a guardarAhora(), con un tooltip de mensaje(error) más «Pulsa para reintentar.»: un autoguardado fallido (disco lleno, base de datos bloqueada) se puede reintentar ahí mismo en vez de esperar al siguiente cambio.
vista === "deck": ResizablePanelGroup horizontal con izquierda (lista, 22 % por defecto, mín. 260 px), centro (vista previa, mín. 360 px), derecha (exportar, 22 % por defecto, mín. 260 px) y dos asas con agarraderas. Los tamaños se persisten en localStorage["proxybuilder.paneles"] mediante useDefaultLayout. Las demás vistas son un simple cambio según vista: disenar, mesa y, para cualquier otro valor, biblioteca pintan la pantalla que les pasa App. mesa es MesaScreen (src/features/mesa/mesa-screen.tsx, Mesa de juego): pregunta una vez a Rust con mesa_estado y, hasta saberlo, no pinta nada; sin partida abierta enseña la entrada EntradaMesa y, si la hay, la mesa. Salir de la pestaña deja la partida abierta en Rust y la guarda (guardar() al desmontarse); al volver se retoma tal cual.
La entrada (src/features/mesa/entrada.tsx) tiene la lista de decks de la biblioteca (buscador, «Historial» de las partidas terminadas, el deck abierto más recientemente ya elegido), la tarjeta «Partida a medias» cuando hay una partida guardada («Seguir la partida» / «Descartar»), el nombre del jugador, el formato, la lista para jugar (obligatoria en Commander si el deck pasa de 100 cartas), las vidas, el mulligan, las ayudas del turno, la intensidad del sparring y «Tapete de este deck…», y cuatro formas de empezar: «Solitario» (pregunta antes si sustituiría la partida guardada, igual que empezar una partida local), «Con amigos…» (abre «Amigos», Amigos y mensajes), «Partida local…» y «En red…» («En red…» pasa a «Volver a la partida en red» mientras haya una partida en red abierta). Las opciones tienen su propio desplazamiento y los botones de empezar se quedan fijos debajo; un botón de empezar desactivado dice por qué en su tooltip. La ventana tiene un mínimo de 960 x 640 y un tamaño por defecto de 1440 x 900 (src-tauri/tauri.conf.json).
AppShell lee useFormato() (Móvil) y pinta una de tres disposiciones de las mismas pantallas. Escritorio es exactamente la disposición descrita arriba.
| Formato | Disposición |
|---|---|
| Escritorio | Cabecera con el selector de cinco pantallas y tres paneles redimensionables en Deck. |
Tableta con dedo (tamano === "tableta" && tactil) |
Cabecera compacta (el título y los textos de los botones se encogen), dos paneles en Deck (lista y hojas; sus anchos se guardan en proxybuilder.paneles-tableta) y Exportar en una hoja lateral (Hoja) que abre un botón «Exportar» de la cabecera. Una ventana de escritorio estrecha con ratón se queda con la disposición de escritorio, porque la de tableta es para el dedo, no para el sitio. |
| Móvil | Una cabecera de 3 rem con la forma de BarraSuperior (el nombre del deck como campo editable y el indicador de guardado como icono, SaveIndicator compacto; solo en Deck), la pantalla, y una barra de pestañas de abajo (BarraInferiorMovil): Deck, Diseñar, Biblioteca, Buscar, Jugar y «Más». De lado, la barra es un carril a la izquierda (lado) y las pestañas de Deck van dentro de la cabecera. Deck se parte en tres pestañas (Lista, Hojas, Exportar). «Más» abre una hoja con Ajustes, el tutorial de la pantalla en que se está y el botón de actualizar, más el aviso de actualización si lo hay. |
Detalles que explican las decisiones:
- El alto de la raíz con dedo es
min(100%, var(--alto-visible, 100dvh) - env(safe-area-inset-top)):#rootya reserva las zonas seguras (0 en Android, donde la app va siempre a pantalla completa y la actividad deja fuera la muesca; véase Móvil) y, con el teclado abierto,--alto-visible(delib/teclado.ts) lo encoge a lo que se ve. La barra de pestañas se esconde con el teclado (data-teclado). - La raíz del móvil es
w-full overflow-clipy sumain,min-w-0: nada de lo de dentro puede ensanchar el documento y desplazar en horizontal toda la pantalla (clipy nohidden, para que no sea un contenedor de scroll que el navegador mueva al enfocar algo). - El nombre del deck de la cabecera pasa por
useNombreDeck()(lib/use-nombre-deck.ts): el de fábrica, «Deck sin título», se enseña en el idioma de ahora hasta que se renombre el deck; si no se toca el campo, lo guardado no cambia. - Los avisos (sonner) se suben por encima de la barra de pestañas en el móvil (
index.css); si no, quedarían debajo. - Una partida abierta en el móvil esconde la cabecera y la barra de pestañas (
useMesaAPantallaCompleta): la mesa ocupa toda la pantalla y su barra lleva su propio botón de salir, que llama asalirDeLaMesa(). - El «atrás» de Android:
AppShellregistra (solo con dedo) un manejador que, desde Diseñar, vuelve a Deck, y pone «Pulsa otra vez para salir» como el de por defecto, que guarda el deck y la carta abierta antes deapp_salir. Las capas abiertas (diálogos, hojas, menús) las cierra anteslib/atras.ts; véase Móvil. - En el móvil, salir de Diseñar con una carta sin guardar no pregunta: se guarda primero y luego cambia la pantalla (la pregunta «¿Descartar los cambios?» de más abajo es de escritorio y tableta).
- El campo del nombre del deck confirma su texto al irse la app a segundo plano (un oyente de
visibilitychangeen fase de captura), para que el nombre escrito no se pierda antes de quelib/ciclo-de-vida.tsguarde el deck. - El botón de ayuda de la cabecera pasa a ser «Tutorial de esta pantalla» dentro de la hoja «Más» en el móvil.
Cambiar de pestaña desmonta la vista anterior, así que antes se perdía su posición de scroll. AppShell llama a useScrollPorVista(vista, principal) con una referencia a <main>:
-
Apuntar. Un único listener de
scrollen fase de captura sobre<main>(scrollno sube por el DOM) guarda elscrollTopde cada viewport deScrollArea([data-slot="scroll-area-viewport"]) en unMap<Vista, Map<índice, top>>dentro de una referencia. La clave de cada zona de scroll es su orden entre los viewports de<main>; los diálogos van por portal fuera de<main>, así que no cuentan. -
Reponer. Al activarse una vista, un bucle de
requestAnimationFramepone cadascrollTopguardado en cuanto ese viewport es lo bastante alto (scrollHeight - clientHeight >= top), durante un máximo de 2 s (ESPERA_MS). Las listas y la vista previa se llenan después del primer pintado, así que ponerlo una sola vez al montar se quedaría en 0. Unawheel, unpointerdowno unkeydowndentro de<main>detiene el bucle, para no pelearse nunca con el usuario. -
Por qué no se mantienen montadas y ocultas.
display: nonetambién reinicia el scroll, y la vista previa y el diseñador seguirían trabajando por debajo (Vista previa en vivo, Interfaz del diseñador). - Límites. Solo en memoria (se pierde al cerrar la app). Como la clave es el orden, una zona de scroll que aparece o desaparece entre visitas (por ejemplo el formulario de Diseñar según haya o no carta elegida) puede cambiar qué posición recibe cada una.
AppShell pasa el cambio de pestaña por cambiarVista(v). Si la vista actual es disenar y useEdicion.getState().pendiente está puesto (la carta que se diseña tiene cambios sin guardar), el movimiento se aparca en salirDeDisenar y un segundo Confirmar («¿Descartar los cambios?» / «Hay cambios sin guardar en esta carta.») pregunta con tres salidas: Cancelar, Descartar (destructiva, ejecuta el movimiento aparcado) y Guardar y continuar (guarda con pendiente.guardar() y solo se mueve si devolvió true). Por qué existe: DisenarScreen se desmonta al mostrar otra vista, así que antes de esta guarda el borrador se perdía en silencio. Por qué el shell no sabe cómo se guarda una carta: DisenarScreen registra un callback guardar en useEdicion mientras tiene cambios (Stores de estado, Interfaz del diseñador) y el shell solo lo llama.
F11 entra y sale de pantalla completa desde cualquier pantalla de la app de escritorio (Deck, Diseñar, Biblioteca, Buscar, Jugar, con un diálogo abierto o un campo con el foco). No es un atajo de la mesa: iniciarVentana() (la llama main.tsx cuando app_plataforma dice escritorio; nunca en Android ni iOS) engancha un solo keydown en window, en la fase de captura y antes de montar nada, para que ningún diálogo, campo ni capa de la mesa lo frene. Solo F11 a secas (con Ctrl, Alt, Meta o Mayús es otra cosa), preventDefault() siempre, y mantenerla pulsada (e.repeat) no repite el cambio. El botón «Pantalla completa (F11)» de la barra de la mesa y su entrada de menú llaman a la misma alternarPantallaCompleta().
flowchart LR
K["F11 / botón"] --> Q["alternarPantallaCompleta(): en cola"]
Q --> W["isFullscreen() y luego setFullscreen(!)"]
W --> L["leer(): isFullscreen() otra vez"]
R["resize de la ventana (rAF, una vez por fotograma)"] --> L
L --> S["useVentana.completa"]
S --> C["ControlesVentana se pinta o se quita"]
Decisiones e invariantes:
-
El estado se lee, no se supone. La ventana puede salir de pantalla completa sin la app (un atajo del gestor de ventanas, un doble clic) y
window-statela restaura al arrancar, así queuseVentana.completase refresca tras cada cambio y en cadaresize, como mucho una vez por fotograma. Por eso los controles salen cuando la ventana ya es la grande, nunca antes, y nada parpadea sobre una ventana que aún es pequeña. - Los cambios van en cola. Dos pulsaciones de F11 seguidas leerían las dos «no está a pantalla completa» y pedirían entrar dos veces; la segunda parte de lo que dejó la primera.
-
Los controles solo existen a pantalla completa. El sistema esconde entonces los suyos y, sin cerrar ni minimizar, el ratón se quedaría sin salida.
ControlesVentanano pinta nada el resto del tiempo (la barra no pierde sitio) y entra con un fundido corto (animate-in, apagado conprefers-reduced-motion) para que pulsar F11 no haga saltar la barra. Minimizar usaminimize(); el botón del medio sale de pantalla completa; Cerrar usaclose(), nuncadestroy(), así que lo atiendeConfirmarCierreigual que la X del sistema (guarda y pregunta si se perdería algo). -
No en macOS (
esMac()): su pantalla completa ya baja los botones del sistema al llegar el ratón al borde. -
Permisos.
capabilities/default.jsonllevacore:window:allow-set-fullscreen,core:window:allow-minimizeycore:window:allow-close;isFullscreenestá encore:default. Véase Capa de aplicación Rust.
ConfirmarCierre registra una sola vez getCurrentWindow().onCloseRequested(...). Flujo de una petición de cierre:
flowchart TD
A["Petición de cierre"] --> B{"¿Diálogo ya abierto?"}
B -->|"sí"| C["preventDefault, nada más"]
B -->|"no"| D["guardarAhora(): vuelca el mazo"]
D --> E["pendientes(deckSinGuardar) en lib/cierre.ts"]
E --> F{"¿hayPendientes?"}
F -->|"no"| G["return sin preventDefault: el envoltorio de Tauri llama a destroy(), la ventana se cierra"]
F -->|"sí"| H["preventDefault + Confirmar: ¿Cerrar Tapete?"]
H --> I["Cancelar: se queda, vuelve a preguntar en el siguiente intento"]
H --> J["Cerrar igualmente: cancelarTrabajo si hay uno, guarda el mazo, destroy()"]
H --> K["Guardar y cerrar (solo con la carta sin guardar): useEdicion.pendiente.guardar() y cierra; si guardar falla, el diálogo y la ventana se quedan"]
Decisiones e invariantes:
-
Primero se vuelca el mazo. El autoguardado espera 800 ms tras el último cambio (Stores de estado), así que un cambio hecho justo antes de cerrar se perdía.
guardarAhora()corre en cada petición de cierre; si lanza, «No se ha podido guardar el deck: se perderán sus últimos cambios.» pasa a ser una razón más para preguntar (pendientes(true)). -
Razones (
src/lib/cierre.ts,pendientes()/hayPendientes(); cada una es un campo dePendientesy el diálogo lista solo las que se cumplen): un trabajo largo (useTrabajo.actual, con el texto de su fase: resolver, exportar, idioma), una actualización bajando o instalando (useActualizacion.pasoesbajandooinstalando; una actualización solo disponible o ya lista no cuenta), un diálogo abierto con un proceso en marcha o texto sin guardar (lib/dialogos-abiertos.ts, más abajo), la carta que se diseña con cambios sin guardar (useEdicion.pendiente), el mazo que no se pudo guardar y una partida abierta en la mesa (useMesa.activa, motivo «Hay una partida en la mesa: se guarda y podrás seguirla al volver.»). -
La partida se guarda en cada petición de cierre. Justo después de guardar el mazo, el manejador llama a
guardar()desrc/features/mesa/conexion.ts(mesa_guardar), y «Cerrar igualmente» lo vuelve a llamar antes dedestroy(). Una partida abierta sigue contando como razón para que quien juega sepa que la tendrá esperando en «Jugar» (Mesa de juego). -
Sin ninguna razón no sale diálogo. El manejador vuelve sin
preventDefault()y la ventana se cierra como siempre. -
«Cerrar igualmente» es destructiva: cancela el trabajo en marcha con
cancelarTrabajo()(export_cancelojob_cancel), intenta guardar el mazo una vez más y llama adestroy(). Las exportaciones escriben de forma atómica, así que cerrar a mitad de una exportación nunca deja un PDF a medias con su nombre final. Sidestroy()falla, un toast dice «No se ha podido cerrar la ventana.». -
Un segundo intento de cierre con el diálogo abierto se ignora (ref
abierto): se le hacepreventDefaulty no se apila otro diálogo. -
Fuera de Tauri (la vista abierta en un navegador durante el desarrollo)
getCurrentWindow()lanza y se captura: no hay ventana que guardar. - 🛑
destroy()necesita un permiso. Con un escuchante de JavaScript enonCloseRequested, Tauri ya no cierra la ventana por su cuenta; lo hacedestroy()(el envoltorio o el manejador). Ese comando escore:window:allow-destroy, que no forma parte decore:defaultni decore:window:default, así quesrc-tauri/capabilities/default.jsonlo lista expresamente (véase Capa de aplicación Rust). Sin él la ventana no se podría cerrar; la compilación valida el identificador. -
Esta guarda no bloquea al actualizador. «Reiniciar y actualizar» guarda el mazo por su cuenta (
guardarAhora()) y reinicia desde Rust (request_restart), que no pasa por la petición de cierre de la ventana. Deducido: del comportamiento de Tauri; no se ha probado de extremo a extremo. - No cubierto. Deducido: en macOS, Cmd+Q o «Salir» desde el Dock puede que no emitan una petición de cierre de ventana, y entonces la guarda no correría.
Algunos diálogos tienen un proceso largo o un estado sin guardar propio. Se lo dicen a la guarda de cierre con useEstadoParaCerrar({ enCurso, sinGuardar }), llamado con los mismos valores que el componente ya pasa a DialogContent como ocupado / cambiosSinGuardar. El hook guarda los últimos valores en un ref y registra un lector en un Set del módulo (registrarDialogo, que devuelve la función que lo quita); estadoDeLosDialogos() junta con OR todos los lectores. Como quien registra es el Contenido del diálogo, que solo existe mientras está abierto, el registro vive justo lo que el diálogo.
Por qué un hook por diálogo y no dentro de DialogContent: src/components/ui/ es de solo lectura (heredado del sistema de diseño en que se basa la app), así que dialog.tsx no se toca.
| Diálogo | Fichero | enCurso |
sinGuardar |
|---|---|---|---|
| Diseñar el deck | src/features/disenar/lote-dialog.tsx |
en marcha | no |
| Exportar imágenes | src/features/disenar/png-dialog.tsx |
en marcha | no |
| Vincular en bloque | src/features/variants/vincular-lote.tsx |
trabajando | revisando filas (fase === "revisar" con filas) |
| Colecciones | src/features/disenar/colecciones-dialog.tsx |
no | cambiada |
A los diálogos sin el hook no se les pregunta. Deducido: solo tienen la regla del texto escrito de DialogContent para cerrarse ellos, no para cerrar la app.
Tests: src/lib/__tests__/cierre.test.ts (Tests y CI).
-
iniciarAutoguardado()einiciarPlan()(véase Stores de estado). - Un
useDeck.subscribeque, cada vez que cambia el id del mazo abierto, llama arehacerMiniaturas()(src/features/disenar/renumerar.ts) para volver a renderizar las miniaturas de diseño que han quedado obsoletas por un cambio de colección, de letra o de motor. -
abrirDeckInicial(t("Deck sin título")): reabrelast_deck_ido crea un mazo nuevo. El error devuelto (si lo hay) se guarda en el estado del hook. - Por separado, un listener de
keydownparaCtrl/Cmd+Z(deshacer) y su variante conShift(rehacer), desactivado mientras se escribe en un input, textarea ocontenteditable.
Decisión: la ventana se crea oculta con un color de fondo oscuro y se muestra lo antes posible, para evitar un destello blanco.
-
tauri.conf.json:"visible": false,"backgroundColor": "#1d1b19". -
src-tauri/src/lib.rs: el plugin window-state guarda tamaño/posición/estado maximizado de todos los flags salvoVISIBLE(si no, el estado guardado podría mostrar la ventana antes de que se haya pintado).on_page_loadmuestra la ventana enPageLoadEvent::Finished; un hilo en segundo plano la muestra tras 3 s si sigue oculta (la página nunca cargó). -
src/main.tsxtambién llama agetCurrentWindow().show()justo después derender(no espera a un fotograma: WebKitGTK no entregarequestAnimationFramea las ventanas ocultas), yponerTemaAntesDeMontar()ya había puesto los colores correctos, así que no hay destello claro. Véanse Arquitectura del frontend y Temas y skins.
Lo abre el botón de Ajustes (AppShell guarda open en estado local). DialogContent es sm:max-w-3xl con cambiosSinGuardar={false} (no hay nada que perder al cerrar). La cabecera muestra la versión leída una vez con getVersion() de @tauri-apps/api/app («Tapete v4.0.0») y, cuando hay una actualización descargada, el botón ReiniciarParaActualizar. Su descripción dice lo que hay dentro: «Idioma, aspecto, mesa de juego, actualizaciones y almacenamiento.».
Cuerpo, de arriba abajo:
| Sección | Componente | Efecto | Persistencia |
|---|---|---|---|
| Idioma |
SelectorIdioma (src/components/selector-idioma.tsx): un Select con la bandera y el nombre propio de cada idioma, sobre los 15 idiomas de IDIOMAS (src/lib/idiomas.ts) |
Idioma de la interfaz, sin recarga. Primero se carga el diccionario nuevo y el cambio llega cuando está, así que la pantalla nunca enseña textos a medio traducir. |
localStorage["proxybuilder.lang"]; véase Internacionalización. |
| Actualizaciones |
Actualizaciones (actualizaciones.tsx) |
Buscar ahora, descargar, reiniciar; el interruptor de actualización automática. | Interruptor: useUi().actualizarSola en proxybuilder.preferencias. La máquina de estados, en memoria. |
| Nombres en otros idiomas |
NombresIdiomas (nombres-idiomas.tsx) |
Descarga opcional del índice de nombres de MTGJSON. | En disco, por Rust (véase abajo). |
| Apariencia |
Appearance (appearance.tsx) |
Modo, escala, scroll suave, skin, filigrana, fondo, material, efectos, tema de color, «Más temas…». | Claves de localStorage listadas en Temas y skins. |
| Mesa de juego (cerrada al principio) |
AjustesMesa (mesa.tsx) |
Seis grupos, más abajo. |
useUi().mesa en proxybuilder.preferencias, salvo el aspecto de los dados (Rust) y la identidad en red (Rust); véase Mesa de juego. |
| Almacenamiento |
Almacenamiento (almacenamiento.tsx) |
Tamaño de la caché de miniaturas y «Limpiar caché de miniaturas»; «Liberar espacio» (abre DialogoLiberar: imágenes que no usa ningún deck y optimizar las de los decks). Véase más abajo. |
Nada (solo acciones). |
En el móvil el diálogo va a pantalla completa (DialogoAdaptable) y las secciones de arriba no se apilan: ListaSecciones enseña una fila grande por sección (Idioma, Actualizaciones, Nombres en otros idiomas, Apariencia, Mesa de juego, Almacenamiento) y tocar una entra en ella, con BarraSeccion para volver. El «atrás» de Android dentro de una sección vuelve a la lista en vez de cerrar el diálogo (volverConEscape, Móvil). Dentro de una sección, Seccion admite fija (siempre abierta y sin botón de plegar, porque la sección ya tiene su pantalla) y Fila pasa su ayuda por ConAyuda, de modo que la explicación de un ajuste se alcanza con un toque en táctil.
- Llama a
useActualizacion.iniciar()al montar. Sisoportada === falsemuestra «Solo en la app instalada: esta copia no viene de un instalador (por ejemplo, en desarrollo).» y nada más. - «Buscar ahora» (
buscar) está desactivado mientras está ocupado; si se encuentra una versión y la actualización automática está activada, la descarga al instante (como al arrancar). - Cuando hay una versión disponible y la automática está desactivada: un botón «Descargar la {version}». Cuando está descargada (
lista/instalando): botón «Reiniciar y actualizar» (descarta el toast"actualizacion", llama ainstalar()y muestra el error en un toast si falla). - Aparece una barra
Progressdurante la descarga; el último error se imprime conmensaje(error); las notas de la release (texto Markdown, en inglés) aparecen en unScrollAreacuando existen; «Tienes la última versión ({version}).» cuando está al día. - La línea de estado dice que, en las instalaciones
.deb/.rpm, el sistema pedirá la contraseña al reiniciar (ActualizacionInfo.pidePermiso).
Los detalles de la máquina de estados están en Stores de estado; el actualizador de Rust, las comprobaciones de firma y el formato de las releases, en Versiones y actualizaciones automáticas. La instalación se rechaza con Busy mientras corre un trabajo largo (el mazo se guarda antes y el paso vuelve a lista). Con una partida abierta en la mesa la pantalla ni lo pide: la instalación queda en cola (alSalirDeLaMesa, toast «Se instalará al salir de la mesa.») y se hace al salir de la mesa; Rust la sigue rechazando con Busy motivo: partida como red de seguridad. Buscar y bajar no se bloquean.
En Android el actualizador funciona distinto, con la misma pantalla. La sección «Actualizaciones» dice «Android te pedirá confirmar cada instalación»; el botón dice «Instalar» en vez de «Reiniciar y actualizar» (instalaElSistema en el store del actualizador, del campo instala_el_sistema de la información de actualización de Rust), porque no se reinicia nada: la app descarga el APK, vuelve a comprobar su firma y se lo da al instalador del sistema, que pide confirmación y relanza la app él mismo. Si el sistema aún no deja a esta app instalar paquetes, Rust abre ese ajuste de Android y contesta Unsupported con motivo: instalar_permiso; la pantalla no lo muestra como un error y la persona vuelve a tocar «Instalar» después. Cualquier otro fallo ofrece «Bajar desde GitHub» (update_open_release). El empaquetado y la firma, en Versiones y actualizaciones automáticas.
El mismo SelectorIdioma elige el idioma de diseño en el panel de exportar y en la vista previa de Diseñar (src/features/export/export-panel.tsx, src/features/disenar/vista-previa.tsx): los idiomas de la interfaz, del diseño y del deck comparten los códigos de Scryfall de src/lib/idiomas.ts, así que un solo componente sirve para los tres. Las banderas son imágenes de Noto Color Emoji (Bandera en src/components/emoji.tsx), no la letra de emoji del sistema, porque la Segoe UI Emoji de Windows no tiene banderas; véase Arquitectura del frontend.
Descarga opcional (aproximadamente del tamaño que muestra el botón, «Descargar (unos {mb} MB)») del índice de nombres por idioma de MTGJSON, guardado como SQLite por Rust, de modo que las cartas escritas por su nombre impreso en español (u otro idioma) que Scryfall no tiene se puedan encontrar en local, tanto en la lista como al vincular. Sin él, la app pregunta en línea a magicthegathering.io, que es más lento. Comandos: names_index_info, names_index_download (evento de progreso names://progress con fases; durante la fase de «lectura» la barra está llena y el texto dice «Preparando el índice…»), names_index_remove (tras una confirmación, «¿Quitar los nombres en otros idiomas?», porque recuperarlos es volver a descargarlos). Muestra el número de nombres descargados. Véanse Fuentes externas e Identificación de cartas y OCR.
Documentada control por control en Temas y skins (modo, tamaño de 70 a 150 %, scroll suave, Plano/Zeus/Hera, filigrana, fondo, material, desenfoque/grano/sombra, «Incluye los paneles», cuadrícula de temas, «Más temas…»).
Scroll suave (proxybuilder.smoothScroll, activado por defecto) monta SmoothScroll (src/lib/SmoothScroll.ts) mediante SmoothScrollClient: la rueda anima el scrollTop nativo del ancestro con scroll más cercano. Al desactivarlo se destruye la instancia.
Cada control escribe un campo de useUi().mesa (PreferenciasMesa en src/stores/ui-store.ts, valores de fábrica en MESA_DE_FABRICA) con setMesa, salvo que se diga otra cosa. Al leerlas, las preferencias guardadas se funden sobre MESA_DE_FABRICA, así que un campo añadido después toma su valor de fábrica.
| Grupo | Controles (campo) |
|---|---|
| «Jugar» | Ofrecer el bono de auras y equipos al adjuntarlos (ofrecerBono, sí); ver la vida del fantasma en solitario (fantasma, sí); en red, ver cómo mueven los demás y mostrarles mi cursor (movimientoEnVivo, sí); ruleta al sortear quién empieza, solo con varios jugadores (ruletaEmpieza, sí); en partida local, pasar a mirar al jugador del turno (seguirTurnoLocal, no); intensidad del sparring al empezar en solitario (sparring, apagado; la entrada tiene el mismo selector); mantener pulsado abre el menú de la carta (clicSostenido, no). |
| «Tablero» | Tamaño de las cartas del campo (tamanoCartas, de 25 a 200 %, 100); tamaño de la vista grande (vistaGrande, de 200 a 500 px, 320); panel de estado junto a la vista grande (panelEstado, sí). |
«Dados y monedas» (aspecto-dados.tsx, AjustesDados) |
El aspecto de tus dados y tu moneda, con una muestra en CSS 3D que se tira con «Probar» (con sonido): color del dado (el del jugador o uno propio), color de los números, acabado (liso, nacarado, marmol, purpurina, translucido), una imagen propia en cada cara (sustituye al color y al acabado) y el metal de la moneda (oro, plata, cobre). No está en useUi: useAspectoDados (src/features/mesa/dados/aspecto.ts) lo lee y lo escribe con mesa_dados_get, mesa_dados_set y mesa_dados_imagen; Rust lo guarda en app_settings, clave mesa.dados, y la imagen en el almacén de blobs (src-tauri/src/mesa/dados.rs). Los cambios se mandan 250 ms después del último. |
| «Sonidos» | Un interruptor por grupo de sonidos: dados y monedas (sonidoDados), cartas: barajar, robar, girar, jugar (sonidoCartas), pasar el turno y cuando es el tuyo (sonidoTurno), mensajes y fantasma del sparring (sonidoChat), todos encendidos; un volumen para todos (volumen, de 0 a 100 %, 60). |
«Identidad en red» (identidad-red.tsx) |
Exportar la identidad en red de la instalación a un fichero JSON o importar otra (pregunta antes; la de ahora se queda como copia en la carpeta de datos de la app). Lo hace Rust (red_identidad_exportar, red_identidad_importar); importar se rechaza con Busy mientras haya una partida o una sala abiertas. No es una preferencia. |
| «Rendimiento» | Calidad de las imágenes del campo (calidad: 146, 244 o 488 px de ancho, 244); dados y monedas (dados: auto, 3d con física, css 3D ligero, 2d; auto); reducir animaciones (reducirAnimaciones, no); sombras (sombras, sí); contador de fotogramas (fps, no); «Modo rendimiento (todo al mínimo)» aplica MODO_RENDIMIENTO (src/features/mesa/fps.tsx: calidad 146, sin sombras, animaciones reducidas, sin panel de estado). |
PreferenciasMesa guarda además campos sin control aquí: sparringVisto (ya salió la tarjeta de «Cómo funciona» del sparring), trackers (los contadores de jugador que se ven junto a la vida) y disposicion (el reparto de los tableros, recordado por número de jugadores), que se cambian desde la propia mesa.
| Pieza | Comportamiento |
|---|---|
useActualizacionAutomatica() |
Solo cuando useUi().actualizarSola es verdadero (por defecto). Tras 8 s (PRIMERA_MS) y después cada 6 h (CADA_MS): iniciar(); si no está soportada, se detiene; buscar(); si existe una versión, bajar(). Los fallos de red o la ausencia de release son silenciosos. Los temporizadores se limpian al desactivar el interruptor. |
useAvisosActualizacion() |
Un único useActualizacion.subscribe, montado una vez en App: un toast de carga «Descargando Tapete {version}…» con el porcentaje como descripción mientras bajando; un toast de éxito persistente «Tapete {version} está lista» con una acción Reiniciar cuando termina la descarga (una vez por versión); un toast de error «No se ha podido descargar la actualización.» con el error traducido si falla la descarga. Todos usan el id de toast "actualizacion" para sustituirse entre sí. |
BotonActualizar |
Botón outline de la barra superior «Actualizar a {version}», visible solo en lista/instalando; spinner mientras se instala; el tooltip dice si el sistema pedirá una contraseña. |
PuntoActualizacion |
Puntito sobre el botón de Ajustes: ámbar y pulsante mientras bajando; del color primario del tema en lista/instalando. |
useAvisoAjustes() |
Texto de tooltip/aria para el botón de Ajustes mientras hay una actualización en curso («Descargando la {version}…» / «La {version} está lista: reinicia desde Ajustes.»). |
Los textos de los toasts leen el idioma actual a través de una ref (textos.current = lang), así que un cambio de idioma durante la descarga se respeta sin volver a suscribirse.
| Ajuste | Ubicación en la UI | Guardado en |
|---|---|---|
| Idioma de la interfaz | Ajustes |
localStorage proxybuilder.lang
|
| Tema, modo, skin, fondo, material, desenfoque, grano, sombra, escala, flags, scroll suave | Ajustes > Apariencia |
localStorage proxybuilder.theme / .mode / .skin / .bg / .material / .blur / .noise / .shadow / .scale / .fxSidebar / .heraFiligree / .smoothScroll
|
| Temas de tweakcn instalados | Ajustes > Más temas | Ficheros de Rust en la carpeta de datos de la app temas/, con espejo en proxybuilder.temasInstalados
|
| Actualización automática | Ajustes > Actualizaciones |
useUi().actualizarSola (proxybuilder.preferencias) |
| Ajustes de la mesa de juego | Ajustes > Mesa de juego |
useUi().mesa (proxybuilder.preferencias) |
| Aspecto de los dados y la moneda | Ajustes > Mesa de juego > «Dados y monedas» |
app_settings de Rust, clave mesa.dados (la imagen propia, en el almacén de blobs) |
| Nombre del jugador en la mesa | Entrada de la pestaña Jugar |
localStorage proxybuilder.mesa.nombre
|
| Tapete de la mesa (por deck y general) | Pestaña Jugar: «Tapete de este deck…» y la barra de la mesa |
app_settings de Rust, claves mesa.tapete.<id del deck> y mesa.tapete (blob protegido en blob_refs) |
| Pantallas cuyo tutorial ya salió | automático (primera visita) |
useUi().tutorialesVistos (proxybuilder.preferencias) |
| Índice de nombres de MTGJSON | Ajustes | Rust (fichero descargado, índice SQLite) |
| Idioma de las cartas, política de imagen, añadir fichas, calidad de la vista previa, preguntar antes de sustituir | Panel de la lista > Opciones |
useUi() (proxybuilder.preferencias) |
| Zoom de la vista previa, superposición de info de carta | Barra de herramientas de la vista previa |
useUi() (proxybuilder.preferencias); «Ver reversos» no se persiste |
| Anchos de los paneles | arrastrar las asas |
localStorage proxybuilder.paneles, .panel-lista, .paneles.disenar
|
| Sección abierta/cerrada | cabeceras de sección |
localStorage proxybuilder.seccion.<id>
|
| Vista, orden, agrupación y tamaño de la biblioteca | Barra de herramientas de la biblioteca |
localStorage proxybuilder.biblioteca (los filtros no se guardan) |
| Último mazo abierto | automático | Rust, clave de app_settings last_deck_id
|
| Última carpeta de importar / última carpeta de exportar | automático | Rust, claves de app_settings carpeta_importar, carpeta_exportar
|
| Tamaño, posición y maximizado de la ventana | automático |
tauri-plugin-window-state (la visibilidad, excluida) |
| Ajustes de impresión (tamaño de página, tamaño de carta, márgenes, sangrado, guías de corte, etc.) y presets de exportación | Panel de exportar | Dentro del mazo (Deck.settings, se guarda con el mazo); presets en Rust (comandos preset_*). Véase Exportación. |
Valores de fábrica que conviene conocer (son decisiones de producto, no accidentes): idioma de la interfaz inglés; tema Nature (un verde, como el logo) en modo oscuro con material mate; zoom de la vista previa 80 % (en el móvil, la primera vez que salen hojas el zoom ajusta la hoja al ancho de la pantalla); calidad de la vista previa 488 px (244 px con dedo en un móvil o tableta; un portátil táctil cuenta como escritorio y se queda en 488); con dedo la mesa de juego arranca con las sombras apagadas y los dados en CSS 3D (mesaDeFabricaPara en ui-store.ts; lo que la persona haya guardado manda); «preguntar antes de sustituir cartas» activado; actualización automática activada. Los valores por defecto de impresión (2 mm de separación, sangrado 0, guías de corte desactivadas, «Rellenar las esquinas» desactivado) viven en el código de layout/exportación, véase Motor de maquetación.
Todos los diálogos de ficheros pasan por src/lib/dialogos.ts; nunca llames directamente a @tauri-apps/plugin-dialog.
| Función | Uso |
|---|---|
abrirParaImportar(opciones) |
Diálogo de abrir que empieza en la última carpeta de importar; devuelve las rutas (array) o null; recuerda la carpeta de la primera ruta. |
guardarParaExportar(opciones, nombre) |
Diálogo de guardar que empieza en la última carpeta de exportar con nombre como nombre de fichero propuesto. |
elegirCarpetaParaExportar() |
Selector de directorio (exportación de imágenes sueltas); también recuerda la carpeta de exportar. |
recordar(tipo, ruta) |
Recordar sin esperar al resultado; se usa cuando se arrastran ficheros sobre la app. |
Mecánica: carpeta_inicial({ tipo, nombre }) (Rust, src-tauri/src/commands/carpetas.rs) devuelve la carpeta guardada si todavía existe (unida con nombre si se da); si no, nombre o null. carpeta_recordar({ tipo, ruta }) guarda el directorio de la ruta (la propia ruta si es un directorio; si no, su carpeta padre) bajo carpeta_importar o carpeta_exportar en app_settings. TipoCarpeta es "importar" | "exportar" (tipo generado). Las carpetas de importar y de exportar son separadas a propósito, para que exportar un PDF al Escritorio no redirija la siguiente importación de imágenes. Los fallos de cualquiera de los dos comandos se tragan (el diálogo simplemente se abre en su ubicación por defecto). Lo usan features/decklist/acciones.ts, features/export/acciones.ts, features/export/menu-exportar.tsx, features/library/library-screen.tsx y los diálogos del diseñador. Tests: src/lib/__tests__/dialogos.test.ts y el test unitario la_carpeta_de_una_ruta de commands/carpetas.rs.
Dos acciones de mantenimiento viven donde son relevantes y además están juntas en Ajustes > «Almacenamiento» (src/features/settings/almacenamiento.tsx), que es donde se buscan; los dos sitios llaman a los mismos comandos:
-
Limpiar la caché de miniaturas («Limpiar caché de miniaturas»): en el panel de la lista (
decklist-panel.tsx) y en Ajustes (que además enseña el tamaño actual, «Caché de miniaturas: {mb}»), un diálogo de confirmación muestra el tamaño y el número de ficheros decache_statsy luegocache_clearvacía solo la caché de renderizados derivados; las imágenes de los mazos nunca se tocan. -
Liberar espacio: la pantalla de la biblioteca y Ajustes abren el mismo diálogo (
features/library/liberar-dialog.tsx), que también optimiza las imágenes de los decks (Almacenamiento y blobs);blob_gcelimina los blobs que ningún mazo referencia (Rust conserva los recientes durante 24 h); el toast informa del número y de los megabytes.
Motivo: las imágenes de los mazos viven en el almacén de blobs (son datos), la caché solo guarda derivados y siempre se puede borrar. Véase Almacenamiento y blobs.
| Situación | Comportamiento |
|---|---|
localStorage bloqueado o corrupto |
Toda lectura/escritura va en try/catch; se aplican los valores por defecto durante la sesión. |
Falla getVersion()
|
Simplemente no se muestra la etiqueta de versión. |
| Falla la comprobación de actualización (sin conexión, sin release, sin paquete para este instalador) | Silencioso en segundo plano; el «Buscar ahora» manual muestra el error traducido (Network, NotFound...). |
| Falla la instalación (p. ej. se cancela la petición de contraseña) | El estado vuelve a lista, toast de error; se puede reintentar sin volver a descargar. |
| Actualizar mientras se exporta un PDF o hay una partida abierta en la mesa |
update_install devuelve Busy (con una partida, motivo: partida); la pantalla no lo pide mientras la mesa está abierta: la instalación queda en cola y se hace al salir de la mesa. |
| Cerrar la ventana con un trabajo, una descarga, una carta sin guardar, un diálogo ocupado, el mazo sin poder guardarse o una partida abierta |
ConfirmarCierre pregunta; «Cerrar igualmente» cancela el trabajo, guarda la partida y cierra. |
| Cerrar la ventana sin nada pendiente | Se vuelca el mazo y la ventana se cierra sin diálogo. |
destroy() falla |
Toast «No se ha podido cerrar la ventana.». |
| El tema instalado seleccionado ya no existe |
SincronizarTemas restablece Nature. |
- Ajuste nuevo: si afecta al aspecto, añádelo solo en código adyacente a
ThemeProvidermediante CSS nuevo/tema-inicial.ts(el propio proveedor es de solo lectura); si es una preferencia de lista/vista previa, añádela aPreferenciasdeuseUi(Stores de estado); si el backend debe conocerla entre sesiones, usaapp_setting_get/seto un comando dedicado. - Sección nueva de Ajustes: añade un componente de tarjeta en
src/features/settings/y renderízalo enSettingsDialog; el texto, port(); el scroll ya lo aportaDialogCuerpo.
- Arquitectura del frontend, Stores de estado, Temas y skins, Internacionalización, Tutoriales
- Versiones y actualizaciones automáticas, Almacenamiento y blobs, Fuentes externas
- Interfaz del diseñador, Mesa de juego, Juego en línea, Capa de aplicación Rust, Tests y CI
- Guía de usuario, Exportación, Móvil
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
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