Saltar al contenido
Nelkano Emulador multisistema

SDK de Nelkano

Añadir botones y ajustes

Devuelve un botón o ajuste desde contributions(context), indicando dónde aparece y qué acción llama. Nelkano lo dibuja con el estilo de la app.

const contribution = { moduleId: 'example.tools', id: 'open', slot: 'SETTINGS_SECTION', section: 'Biblioteca', label: 'Mis herramientas', actionId: 'open', icon: 'standard:settings' };

Elegir dónde aparece

  • HOME_SECTION: sección en Home.
  • LIBRARY_ACTION: botón en los filtros de Biblioteca, alineado con los controles nativos y con la altura del tema.
  • LIBRARY_SELECTION_ACTION: acción para los juegos seleccionados.
  • GAME_HEADER_ACTION: icono en la cabecera de la ficha.
  • GAME_ARTWORK_ACTION: botón en la esquina inferior de la carátula.
  • GAME_LAUNCH_ACTION: acción dentro de la ficha.
  • SAVE_ACTION: acción sobre un guardado.
  • SETTINGS_SECTION: fila en Ajustes → General.
  • SYSTEM_ACTION y SYSTEM_SETTINGS_SECTION: acciones y ajustes de un sistema.
  • EMULATION_SETTINGS_SECTION: ajuste durante una partida.
  • PLAYER_ACTION y PLAYER_MENU: acciones durante la partida y en su menú.
  • PLAYER_SYSTEM_BUTTON: botón sobre el área de emulación.

section agrupa filas dentro de General; no crea una pestaña. Para un interruptor, usa kind:'toggle' y checked. La acción recibe el nuevo valor en payload.checked.

Abrir un formulario

Desde dispatch, devuelve un panel con title, fields, rows y actions. Puedes añadir campos de texto, selección y JSON. Devuelve {kind:'close-panel'} para cerrarlo. El ejemplo mínimo contiene un panel completo.

enabled desactiva una acción. visibility filtra por sistema, capacidades o disponibilidad de juego, sesión y conexión. Para limitarla a un juego, comprueba context.gameId en contributions(context): visibility no filtra por ese ID. Vuelve a comprobar la disponibilidad al ejecutar.

Integrar una acción con los controles de la app

  • PLAYER_SYSTEM_BUTTON: payload.controlId reutiliza un control existente.
  • GAME_LAUNCH_ACTION: payload.primaryLaunch:true sitúa la acción junto a Jugar.
  • PLAYER_MENU: payload.menuAction con save, load o reset utiliza las tarjetas del menú de guardados.

Estos valores eligen ubicación y estilo. actionId sigue llamando a tu módulo: debes implementar la operación.

Formulario con texto, casilla y selector (ejemplo).
Formulario con texto, casilla y selector (ejemplo).

Mostrar una carátula o captura

  • En una fila, thumbnail:{gameId,slot} muestra la captura junto al texto.
  • slot:null usa la carátula actual; añade original:true para la portada original.
  • En el panel, preview:{gameId,slot} muestra una imagen grande arriba.

Usa los IDs devueltos por la app, sin pasar rutas ni PNG. Una captura requiere saves.read; una carátula, library.read. Si falta la imagen, se muestra la portada de reserva.

Añadir acciones junto al buscador

LIBRARY_HEADER_ACTION añade acciones al buscador de Biblioteca y Guardados. Comprueba surface:library o surface:saves para decidir dónde contribuir.

LIBRARY_ACTION permanece en los filtros de Biblioteca. SAVE_ACTION conserva las acciones de Guardados sin saveId. El host reúne buscador, importar y filtros en una fila horizontal; en vertical utiliza dos.

La búsqueda de Guardados es local por título y se combina con sistema, orden y filtros del módulo. Restablecer limpia también la búsqueda.

Abrir una página completa

Devuelve kind:page, title, fields, rows y actions. La página mantiene la navegación; kind:panel abre un diálogo.

Los campos admiten también password, email y checkbox. En una casilla, initial es booleano. En un select puede ser texto, número finito, booleano o null.

Conserva el tipo de cada opción: initial:0 selecciona el número cero; initial:"0", un texto. Aplicar sin editar y onChangeAction envían ese mismo tipo. Un botón con submitFields:false puede navegar aunque queden campos sin completar.

Responder mientras se edita

onChangeAction envía las ediciones tras 350 ms sin cambios, incluso al volver al valor anterior. Actualizar initial desde el módulo no genera una edición. En un selector se envía el valor con su tipo.

onChangeMinLength fija la longitud mínima; usa 0 si necesitas recibir una búsqueda vacía.

Colocar acciones y filas

  • placement:header sin icono mantiene una acción encima del formulario.
  • En un panel, hasta dos acciones de cabecera pueden usar icon:profile o icon:refresh. Aparecen junto al título fijo, usan label como nombre accesible y tienen un área táctil de 48 dp.
  • Una fila con avatar y presentation:compact reduce el avatar y ajusta el texto de los botones.
  • En acciones de fila, placement:overflow añade la acción al menú ⋮ y conserva payload, confirmación, condiciones y permisos. No se admite en acciones superiores; las acciones de fila no admiten icon.
  • position:header sitúa una fila antes de los campos.

Mostrar avatares

Una fila admite avatar con label, color, imagen PNG en Base64 o url. La imagen local tiene prioridad.

Android carga de forma asíncrona las URL del origen de cuenta permitido y requiere network.account. Studio no descarga imágenes remotas.

Crear una página de ajustes

Usa kind:"page",layout:"settings" para tener cabecera y botón de volver. En un selector, presentation:"tabs" muestra pestañas; initial selecciona la activa y onChangeAction recibe su valor sin añadir comillas.

En rows, elige una presentación:

  • section: encabezado.
  • actions: botones agrupados en una fila, del mismo tamaño y respetando primary.
  • setting: título, subtitle y value, con badge o icon opcional.
  • list: fila sencilla que se puede pulsar.

La primera acción se ejecuta al pulsar una fila de ajuste o lista. Su icono puede ser un recurso declarado en files o un icono standard. placement:"footer" sitúa una acción al pie del formulario.

Mantener visibles el título y los botones de un panel

Todos los kind:"panel", con o sin layout:"form", comparten el mismo diálogo en Android, Desktop y Studio: título y × fijos, cuerpo desplazable y acciones placement:"footer" al pie. Las demás acciones, incluidas las de filas, permanecen en el cuerpo.

Las páginas conservan su navegación; layout:"settings" se utiliza sólo con kind:"page".

Controlar el cierre y conservar lo escrito

dismissAction:"close" utiliza una acción existente con submitFields:false para la × y para cerrar el diálogo, respetando su confirmación.

Sin dismissAction, el host sólo recoge una acción inequívoca de cerrar/cancelar que no envíe campos ni pida confirmación. Si hay varias candidatas, conserva los botones y la × cierra desde el host. No se permite cerrar mientras se procesa una acción.

Asigna un id estable al formulario: el borrador se conserva al actualizar el mismo módulo, formulario y sesión. Cambiar de formulario o sesión lo reinicia. En paneles antiguos sin ID se distinguen título y esquema de campos.

Los avisos nativos y los formularios de cuenta, colecciones, mandos, streaming, parches y módulos utilizan esta presentación. Los lienzos del editor y las superficies de vídeo o mando mantienen su función.

Mostrar campos según una elección

Usa visibleWhen:{field:"role",values:["host"]}, o combina condiciones con {all:[...]} y {any:[...]}. El campo referido debe ser un selector o una casilla del mismo formulario. Los valores deben existir entre sus opciones; las casillas usan booleanos.

Las condiciones se evalúan localmente al seleccionar, sin ejecutar JavaScript ni consultar el servidor. Admiten cuatro niveles, 32 nodos por condición y 32 valores por hoja.

Un campo oculto conserva el borrador, pero no se valida como requerido ni se envía en el payload.

Habilitar una acción según el formulario

enabledWhen utiliza las mismas condiciones y se combina con enabled. description añade una ayuda de hasta 256 caracteres bajo el botón.

Son condiciones de presentación: valida de nuevo los parámetros y permisos al ejecutar. Para elecciones cortas, como Crear partida/Unirse o 2/3/4, puedes usar kind:"select",presentation:"tabs". El borrador resiste las actualizaciones de estado y se reinicia al cambiar la sesión.

Crear componentes y ampliar pantallas

presentation.extend permite ui.page, mount, patch, remove, open, close y targets. Puedes crear secciones, filas, botones, formularios, pestañas, modales, imágenes PNG y lienzos con figuras. dispatch recibe acciones y valores.

  • Monta contenido en home/library/game/saves/settings/emulation.before y .after.
  • Usa .content para ocultar y sustituir el cuerpo.
  • Los componentes publicados ofrecen puntos antes/después y cambios de texto, visibilidad, disponibilidad y actionId. También puedes ampliar componentes de otros módulos.

Los conflictos entre autores se rechazan. Desactivar retira los cambios. Límites: 32 registros por módulo, 256 globales, 96 KiB de contenido, 512 nodos y profundidad 16.

La app y Studio comparten el renderizador. Los componentes se programan en JavaScript o TypeScript; no ejecutan HTML arbitrario. Revisa el resultado en el visor.

Fijar las acciones al pie de un modal

En presentation.extend, kind:"modal" usa el mismo marco. label es el título y actionId recibe el cierre. Sólo sus hijos directos pueden usar placement:"footer"; el resto se desplaza con el contenido.

const dialog = {id:"dialog",kind:"modal",label:"Editar nombre",actionId:"cancel",children:[ {id:"name",kind:"field",label:"Nombre",value:"Ada"}, {id:"actions",kind:"row",placement:"footer",children:[ {id:"save",kind:"button",label:"Guardar",actionId:"save"} ]} ]};

El host rechaza placement fuera de un hijo directo de modal o con otro valor. Las acciones siguen sujetas a la propiedad del módulo, el registro, la revisión y sus permisos.

Usar imágenes generadas o descargadas

  1. Genera o descarga el PNG y guárdalo mediante files.
  2. Añade el nodo {id:"imagen",kind:"image",resource:"file:imagen.png"}.
  3. Declara storage.files y presentation.extend.

El host valida formato y dimensiones. La imagen queda ligada al módulo/autor y a la revisión del registro de interfaz; otro módulo no puede leerla. Funciona en la app y Studio. También puedes componer nodos y dibujar figuras sin generar una imagen.

Recorrer un catálogo por páginas

ui.targets({offset,limit}), entities.types({offset,limit}) y services.list(name,{offset,limit}) devuelven 32 entradas por defecto, con un máximo de 128 y 112 KiB.

Aumenta offset por el número de entradas recibidas hasta obtener una página vacía. Una página puede terminar antes de limit por su tamaño.