API del puente de JavaScript
Archivos: abiertos con esta app
Recoge lo que otra app entregó mediante "Abrir con", en fragmentos que la página va pidiendo.
AppMint.getOpenedFile ayudante#
AppMint.getOpenedFile()
Ejemplo
Entrega a tu página el archivo que se compartió con la app, o que se abrió con ella ("Abrir con…"), por tarde que arranque tu página.
Devuelve: Promise<object | null>: null cuando la app se abrió normalmente. El objeto tiene token, name, mimeType, size, kind, url, file, getFile(), más text (archivos de texto de hasta 32 MB) o base64 (otros archivos de datos de hasta 8 MB) cuando es barato. kind es 'image' | 'video' | 'audio' | 'document' | 'data'. Necesita: activar Recibir contenido compartido de otras apps al compilar y marcar los tipos de archivo que acepta tu app.
async function handleStartFile() {
if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return; // not in the app, or feature off
const f = await AppMint.getOpenedFile();
if (!f) return; // opened normally
console.log(f.name, f.mimeType, f.size, f.kind);
if (f.kind === 'video') {
document.querySelector('video').src = f.url; // streams from disk, can seek
} else if (f.kind === 'image') {
document.querySelector('img').src = f.url;
} else if (f.kind === 'document') {
const file = await f.getFile(); // a real File
await upload(file);
} else {
const text = typeof f.text === 'string' ? f.text : await f.file.text();
importData(text);
}
}
handleStartFile();O escucha: el evento (y el gancho opcional window.onAppMintFileOpen) se dispara una vez por cada archivo nuevo, con el mismo objeto que detail:
window.onAppMintFileOpen = function (f) { console.log('hook:', f.name); };
window.addEventListener('appmint:fileopen', function (e) {
const f = e.detail; // {token, name, mimeType, size, kind, url, file, getFile, text?, base64?}
showFile(f);
});
window.addEventListener('appmint:fileopenerror', function (e) {
alert('Could not read ' + e.detail.name + ': ' + e.detail.error); // detail: {name, error}
});En una app React, un pequeño hook recoge el archivo por tarde que se monte el componente, y sigue atento a los nuevos:
import { useEffect, useState } from 'react';
export function useOpenedFile() {
const [file, setFile] = useState(null);
useEffect(function () {
const A = window.AppMint;
if (!A || typeof A.getOpenedFile !== 'function') return;
let alive = true;
A.getOpenedFile().then(function (f) { if (alive && f) setFile(f); });
function onOpen(e) { setFile(e.detail); }
window.addEventListener('appmint:fileopen', onOpen);
return function () { alive = false; window.removeEventListener('appmint:fileopen', onOpen); };
}, []);
return file;
}Notas: Las imágenes, los videos, el audio y cualquier archivo de más de 64 MB llegan solo como url (file es null); llama a f.getFile() cuando de verdad necesites los bytes. Los archivos de datos y los documentos también llegan como f.file. El archivo se conserva para tu página hasta que lo pidas, así que una SPA que se monta tarde igualmente lo recibe; cada vez se devuelve el más reciente. Cuando la app ya está abierta y llega otro archivo, el evento se dispara de nuevo. Llama a AppMint.releaseOpenedFile(f) al terminar para liberar la copia en caché.
AppMint.getOpenedFiles ayudante#
AppMint.getOpenedFiles()
Ejemplo
Entrega a tu página todos los archivos compartidos con la app, o abiertos con ella, durante esta sesión: útil cuando el usuario comparte varias fotos a la vez.
Devuelve: Promise<Array> de los mismos objetos que devuelve AppMint.getOpenedFile() (token, name, mimeType, size, kind, url, file, getFile(), y text / base64 cuando es barato). Un arreglo vacío cuando no se recibió nada. Necesita: activar Recibir contenido compartido de otras apps al compilar y marcar los tipos de archivo que acepta tu app.
async function importShared() {
if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
const files = await AppMint.getOpenedFiles();
const gallery = document.getElementById('gallery');
files.forEach(function (f) {
if (f.kind === 'image') {
const img = document.createElement('img');
img.src = f.url;
img.alt = f.name;
gallery.appendChild(img);
} else {
console.log('received', f.name, f.size + ' bytes');
}
});
}
importShared();Los archivos nuevos que llegan mientras la página está abierta también disparan appmint:fileopen, un evento por archivo:
window.addEventListener('appmint:fileopen', function (e) { addToGallery(e.detail); });Sube todo lo que el usuario compartió en una sola solicitud, como archivos reales:
async function uploadShared() {
if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
const files = await AppMint.getOpenedFiles();
if (!files.length) return;
const form = new FormData();
for (const f of files) {
try {
form.append('files', await f.getFile(), f.name); // url-only media is fetched here
} catch (e) {
console.warn('skipped', f.name, e.message); // released earlier
}
}
await fetch('/api/upload', { method: 'POST', body: form });
}Notas: La lista es una copia; crece a medida que llegan más archivos. Los archivos que liberaste con AppMint.releaseOpenedFile(f) siguen en la lista, pero su url ya no funciona.
AppMint.releaseOpenedFile ayudante#
AppMint.releaseOpenedFile(file)
Ejemplo
Elimina la copia en caché de la app de un archivo que se compartió con la app o se abrió con ella, una vez que tu página termina de usarlo.
Devuelve: nada. Necesita: activar Recibir contenido compartido de otras apps al compilar.
async function importAndForget() {
if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
const f = await AppMint.getOpenedFile();
if (!f) return;
const file = await f.getFile(); // take your own copy of the bytes first
await saveToMyDatabase(file);
AppMint.releaseOpenedFile(f); // then free the cache on disk
}Un video compartido se reproduce desde f.url, así que libéralo solo cuando termine la reproducción:
async function playShared() {
if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
const f = await AppMint.getOpenedFile();
if (!f || f.kind !== 'video') return;
const video = document.querySelector('video');
video.src = f.url;
video.play();
video.addEventListener('ended', function () {
video.removeAttribute('src');
video.load();
AppMint.releaseOpenedFile(f); // the url stops working from here
}, { once: true });
}Tras importar un lote de fotos compartidas, libera todas las copias en caché de una vez:
async function importAllPhotos() {
if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
const files = await AppMint.getOpenedFiles();
for (const f of files) {
if (f.kind !== 'image') continue;
const file = await f.getFile(); // fetched from f.url
await savePhoto(file);
AppMint.releaseOpenedFile(f);
}
}Notas: Pasa el objeto que recibiste (necesita su token). Un File que ya obtuviste sigue siendo válido; el url deja de funcionar, y getFile() rechaza a menos que ya hubiera obtenido el File. No liberes un video que aún estás reproduciendo desde f.url. La app también borra estas copias cuando se cierra su pantalla, así que liberar antes sirve para ahorrar espacio en disco durante una sesión larga.
openedFileChunk puente#
window.WebToApk.openedFileChunk(token: String, offset: String, len: Int): String
Interno de AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La página pide en lugar de recibir: un archivo abierto en arranque en frío queda reservado hasta que se pide, así que una SPA que se monta tarde igualmente lo recibe. Los metadatos de cada archivo incluyen `kind` (image | video | audio | document | data) y `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servida desde la copia en caché con rangos de bytes, de modo que un video compartido se transmite a <video src> y permite saltar. Los archivos de datos y los documentos también se obtienen en porciones MAX_IO_CHUNK hacia un File; los medios y todo lo que supere 64 MB quedan solo como url (getFile() lo obtiene bajo demanda), así que un clip de 300 MB nunca queda en un arreglo JS.
Descrito por su grupo, Archivos compartidos con la app, o abiertos con ella, y no por separado.
Ejemplo
Mecanismo interno detrás de AppMint.getOpenedFile(): devuelve una porción de los bytes de un archivo compartido o abierto, que el ayudante une en un File. Las páginas deben usar la API pública: f.file, f.getFile() o f.url.
Devuelve: (síncrono) una cadena base64 de hasta len bytes desde offset (el offset se pasa como cadena, así que los archivos de más de 2 GB también funcionan); "" al final del archivo, con un token desconocido o liberado, o ante un error de lectura. Necesita: activar Recibir contenido compartido de otras apps al compilar.
La API pública (usa esta):
if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
AppMint.getOpenedFile().then(function (f) {
if (!f) return;
return f.getFile().then(function (file) { console.log('got', file.size, 'bytes'); });
});
}Lo que hace el ayudante con ello, mostrado solo para explicar los argumentos:
if (window.WebToApk && typeof WebToApk.openedFileChunk === 'function') {
const meta = JSON.parse(WebToApk.openedFilesPending() || '[]')[0];
if (meta) {
const b64 = WebToApk.openedFileChunk(meta.token, '0', WebToApk.openedFileMaxChunk());
console.log('first slice is', atob(b64).length, 'bytes of', meta.size);
}
}Para leer un archivo compartido muy grande sin porciones: transmite f.url y muestra el progreso (aquí contando filas de un CSV):
async function countRows() {
if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
const f = await AppMint.getOpenedFile();
if (!f) return;
const res = await fetch(f.url); // served from disk, no base64 round trips
if (!res.ok) throw new Error('shared file unavailable (' + res.status + ')');
const reader = res.body.getReader();
let seen = 0, rows = 0;
for (;;) {
const step = await reader.read();
if (step.done) break;
seen += step.value.length;
for (let i = 0; i < step.value.length; i++) if (step.value[i] === 10) rows++;
showProgress(seen / f.size);
}
console.log(rows + ' rows in ' + f.name);
}Notas: len se limita de 1 byte a 4 MB. Para archivos multimedia, transmitir f.url es mucho más barato que pedir porciones.
openedFileCollected puente#
window.WebToApk.openedFileCollected(token: String)
Interno de AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La página pide en lugar de recibir: un archivo abierto en arranque en frío queda reservado hasta que se pide, así que una SPA que se monta tarde igualmente lo recibe. Los metadatos de cada archivo incluyen `kind` (image | video | audio | document | data) y `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servida desde la copia en caché con rangos de bytes, de modo que un video compartido se transmite a <video src> y permite saltar. Los archivos de datos y los documentos también se obtienen en porciones MAX_IO_CHUNK hacia un File; los medios y todo lo que supere 64 MB quedan solo como url (getFile() lo obtiene bajo demanda), así que un clip de 300 MB nunca queda en un arreglo JS.
Descrito por su grupo, Archivos compartidos con la app, o abiertos con ella, y no por separado.
Ejemplo
Mecanismo interno detrás de AppMint.getOpenedFile(): marca un archivo compartido o abierto como recogido, de modo que ya no se ofrezca como "nuevo" pero se conserve su copia en caché. Las páginas deben usar la API pública; no hay motivo para llamar a esto directamente.
Devuelve: nada. Necesita: activar Recibir contenido compartido de otras apps al compilar.
La API pública (usa esta): cada archivo se recoge una vez y se entrega a tu página:
if (window.AppMint && typeof AppMint.getOpenedFiles === 'function') {
AppMint.getOpenedFiles().then(function (files) {
files.forEach(function (f) { console.log(f.name, f.kind); });
});
}
window.addEventListener('appmint:fileopen', function (e) { console.log('new file', e.detail.name); });Un archivo recogido al inicio está tanto en la lista como anunciado por el evento, así que gestiona cada uno con tu propio token para procesarlo una sola vez:
const handled = new Set();
function handleOnce(f) {
if (handled.has(f.token)) return;
handled.add(f.token);
addToInbox(f);
}
window.addEventListener('appmint:fileopen', function (e) { handleOnce(e.detail); });
if (window.AppMint && typeof AppMint.getOpenedFiles === 'function') {
AppMint.getOpenedFiles().then(function (files) { files.forEach(handleOnce); });
}Un archivo que no se pudo leer también se recoge; se informa una vez mediante appmint:fileopenerror en lugar de appmint:fileopen:
window.addEventListener('appmint:fileopenerror', function (e) {
const d = e.detail; // {name, error}
showBanner('Could not open "' + d.name + '". Try sharing it again.');
console.warn('opened file failed:', d.error);
});Notas: Llamarlo tú mismo ocultaría un archivo de AppMint.getOpenedFile() y del evento appmint:fileopen sin entregárselo a nadie. Para liberar la copia en caché, usa AppMint.releaseOpenedFile(f).
openedFileMaxChunk puente#
window.WebToApk.openedFileMaxChunk(): Int
Interno de AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La página pide en lugar de recibir: un archivo abierto en arranque en frío queda reservado hasta que se pide, así que una SPA que se monta tarde igualmente lo recibe. Los metadatos de cada archivo incluyen `kind` (image | video | audio | document | data) y `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servida desde la copia en caché con rangos de bytes, de modo que un video compartido se transmite a <video src> y permite saltar. Los archivos de datos y los documentos también se obtienen en porciones MAX_IO_CHUNK hacia un File; los medios y todo lo que supere 64 MB quedan solo como url (getFile() lo obtiene bajo demanda), así que un clip de 300 MB nunca queda en un arreglo JS.
Descrito por su grupo, Archivos compartidos con la app, o abiertos con ella, y no por separado.
Ejemplo
Mecanismo interno detrás de AppMint.getOpenedFile(): la porción más grande que openedFileChunk devuelve en una llamada (4 MB). Las páginas deben usar la API pública, que ya lee en porciones de este tamaño.
Devuelve: (síncrono) un número de bytes (4194304). Necesita: activar Recibir contenido compartido de otras apps al compilar.
La API pública (usa esta):
if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
AppMint.getOpenedFile().then(function (f) {
if (f && f.file) console.log('pulled in 4 MB slices:', f.file.size, 'bytes');
});
}La llamada sin procesar, si solo quieres saber el número:
if (window.WebToApk && typeof WebToApk.openedFileMaxChunk === 'function') {
console.log('slice size', WebToApk.openedFileMaxChunk());
}Estima cuántas llamadas al puente hará el ayudante por cada archivo en espera, al perfilar una importación lenta:
if (window.WebToApk && typeof WebToApk.openedFileMaxChunk === 'function') {
const slice = WebToApk.openedFileMaxChunk();
JSON.parse(WebToApk.openedFilesPending() || '[]').forEach(function (m) {
console.log(m.name, (m.size / 1048576).toFixed(1) + ' MB =', Math.ceil(m.size / slice), 'slice(s)');
});
}Notas: Mismo tope que getMaxIoChunkSize() del puente de acceso a archivos.
openedFileRelease puente#
window.WebToApk.openedFileRelease(token: String)
Interno de AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La página pide en lugar de recibir: un archivo abierto en arranque en frío queda reservado hasta que se pide, así que una SPA que se monta tarde igualmente lo recibe. Los metadatos de cada archivo incluyen `kind` (image | video | audio | document | data) y `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servida desde la copia en caché con rangos de bytes, de modo que un video compartido se transmite a <video src> y permite saltar. Los archivos de datos y los documentos también se obtienen en porciones MAX_IO_CHUNK hacia un File; los medios y todo lo que supere 64 MB quedan solo como url (getFile() lo obtiene bajo demanda), así que un clip de 300 MB nunca queda en un arreglo JS.
Descrito por su grupo, Archivos compartidos con la app, o abiertos con ella, y no por separado.
Ejemplo
Mecanismo interno detrás de AppMint.releaseOpenedFile(f): elimina la copia en caché de un archivo compartido o abierto, por token. Las páginas deben usar la API pública.
Devuelve: nada. Necesita: activar Recibir contenido compartido de otras apps al compilar.
La API pública (usa esta):
async function useAndRelease() {
if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
const f = await AppMint.getOpenedFile();
if (!f) return;
const file = await f.getFile(); // keep your own copy first
await store(file);
AppMint.releaseOpenedFile(f); // calls WebToApk.openedFileRelease(f.token)
}El usuario descarta un archivo compartido sin importarlo: libera la copia en caché de inmediato:
async function offerImport() {
if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
const f = await AppMint.getOpenedFile();
if (!f) return;
const box = showImportPrompt(f.name, f.size); // your own "Import / Discard" UI
box.querySelector('.discard').addEventListener('click', function () {
AppMint.releaseOpenedFile(f);
box.remove();
});
box.querySelector('.import').addEventListener('click', async function () {
await store(await f.getFile());
AppMint.releaseOpenedFile(f);
box.remove();
});
}Un archivo que la app no puede usar se libera en cuanto se rechaza:
window.addEventListener('appmint:fileopen', function (e) {
const f = e.detail;
if (!/\.(gpx|tcx)$/i.test(f.name)) {
alert(f.name + ' is not a GPX or TCX activity file.');
AppMint.releaseOpenedFile(f);
return;
}
importActivity(f.text); // gpx/tcx arrive as text
});Notas: Tras liberar, f.url devuelve un error y openedFileChunk devuelve "" para ese token. Un objeto File que ya tengas no se ve afectado.
openedFilesPending puente#
window.WebToApk.openedFilesPending(): String
Una página ajena no recibe nada: los archivos quedan reservados para la página propia de la app.
Ejemplo
Mecanismo interno detrás de AppMint.getOpenedFile() / AppMint.getOpenedFiles(): lista los archivos compartidos o abiertos que la página aún no ha recogido (solo metadatos, sin bytes). Las páginas deben usar la API pública.
Devuelve: (síncrono) una cadena JSON: un arreglo de {token, name, mimeType, size, isText, kind, url}. [] cuando no hay nada esperando. Necesita: activar Recibir contenido compartido de otras apps al compilar (sin esto la lista siempre está vacía y los ayudantes AppMint no se instalan).
La API pública (usa esta):
if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
AppMint.getOpenedFile().then(function (f) {
if (f) console.log('opened with the app:', f.name, f.kind);
});
}
window.addEventListener('appmint:fileopen', function (e) { console.log('arrived:', e.detail.name); });Solo para depurar: echa un vistazo a lo que está esperando sin recogerlo:
if (window.WebToApk && typeof WebToApk.openedFilesPending === 'function') {
const waiting = JSON.parse(WebToApk.openedFilesPending() || '[]');
console.log(waiting.length + ' file(s) waiting', waiting.map(function (m) { return m.name; }));
}Depura un archivo que nunca llegó a la página: mira qué sigue esperando y si llegará como f.file o solo como f.url:
if (window.WebToApk && typeof WebToApk.openedFilesPending === 'function') {
const STREAM_LIMIT = 64 * 1024 * 1024; // the helper's url-only threshold
JSON.parse(WebToApk.openedFilesPending() || '[]').forEach(function (m) {
const urlOnly = m.kind === 'image' || m.kind === 'video' || m.kind === 'audio' || m.size > STREAM_LIMIT;
console.log(m.name, m.mimeType, m.size + ' bytes',
urlOnly ? 'will come as url only' : 'will be pulled into f.file',
m.isText ? '(text)' : '');
});
}Notas: El ayudante los recoge automáticamente después de load y cada vez que llega un archivo nuevo, así que una página rara vez ve algo aquí. Una vez recogido, un archivo sale de esta lista pero sigue siendo legible hasta que se libere.