API del bridge JavaScript
File: aperti con questa app
Raccogliere ciò che un'altra app ha passato tramite «Apri con», in blocchi che la pagina richiede.
AppMint.getOpenedFile helper#
AppMint.getOpenedFile()
Esempio
Consegna alla tua pagina il file condiviso con l'app, o aperto con essa («Apri con…»), per quanto tardi la pagina si avvii.
Restituisce: Promise<object | null>: null quando l'app è stata aperta normalmente. L'oggetto ha token, name, mimeType, size, kind, url, file, getFile(), più text (file di testo fino a 32 MB) o base64 (altri file di dati fino a 8 MB) quando è economico. kind è 'image' | 'video' | 'audio' | 'document' | 'data'. Richiede: attiva Ricevi condivisioni da altre app quando crei l'app e seleziona i tipi di file che la tua app accetta.
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();Oppure ascolta: l'evento (e l'hook facoltativo window.onAppMintFileOpen) scatta una volta per ogni nuovo file, con lo stesso oggetto di 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}
});In un'app React, un piccolo hook recupera il file per quanto tardi si monti il componente e continua a seguire i nuovi:
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;
}Note: Immagini, video, audio e qualsiasi file oltre 64 MB arrivano solo come url (file è null); chiama f.getFile() quando ti servono davvero i byte. Anche i file di dati e i documenti arrivano come f.file. Il file viene tenuto per la tua pagina finché non lo richiedi, quindi anche una SPA che si monta in ritardo lo riceve; viene restituito ogni volta l'ultimo. Quando l'app è già aperta e arriva un altro file, l'evento scatta di nuovo. Chiama AppMint.releaseOpenedFile(f) al termine per liberare la copia nella cache.
AppMint.getOpenedFiles helper#
AppMint.getOpenedFiles()
Esempio
Consegna alla tua pagina ogni file condiviso con l'app, o aperto con essa, durante questa sessione: utile quando l'utente condivide più foto insieme.
Restituisce: Promise<Array> degli stessi oggetti che restituisce AppMint.getOpenedFile() (token, name, mimeType, size, kind, url, file, getFile(), e text / base64 quando è economico). Un array vuoto quando non è stato ricevuto nulla. Richiede: attiva Ricevi condivisioni da altre app quando crei l'app e seleziona i tipi di file che la tua app accetta.
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();I nuovi file che arrivano mentre la pagina è aperta fanno scattare anche appmint:fileopen, un evento per file:
window.addEventListener('appmint:fileopen', function (e) { addToGallery(e.detail); });Carica tutto ciò che l'utente ha condiviso in una sola richiesta, come file veri:
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 });
}Note: L'elenco è una copia; cresce man mano che arrivano altri file. I file che hai rilasciato con AppMint.releaseOpenedFile(f) restano nell'elenco, ma il loro url non funziona più.
AppMint.releaseOpenedFile helper#
AppMint.releaseOpenedFile(file)
Esempio
Elimina la copia nella cache dell'app di un file condiviso con l'app o aperto con essa, quando la tua pagina ha finito di usarlo.
Restituisce: niente. Richiede: attiva Ricevi condivisioni da altre app quando crei l'app.
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 condiviso viene riprodotto da f.url, quindi rilascialo solo quando la riproduzione è finita:
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 });
}Dopo aver importato un gruppo di foto condivise, libera in una volta tutte le copie nella cache:
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);
}
}Note: Passa l'oggetto che hai ricevuto (ha bisogno del suo token). Un File che hai già recuperato resta valido; url smette di funzionare e getFile() rifiuta, a meno che non avesse già recuperato il File. Non rilasciare un video che stai ancora riproducendo da f.url. L'app svuota queste copie anche quando la sua schermata viene chiusa, quindi rilasciare prima serve a risparmiare spazio su disco durante una sessione lunga.
openedFileChunk bridge#
window.WebToApk.openedFileChunk(token: String, offset: String, len: Int): String
Interno a AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La pagina richiede, invece di ricevere: un file aperto all'avvio a freddo resta in attesa finché non viene richiesto, quindi una SPA che si monta in ritardo lo riceve comunque. I metadati di ogni file includono `kind` (image | video | audio | document | data) e `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servito dalla copia nella cache con supporto agli intervalli di byte, così un video condiviso viene trasmesso in <video src> e si può scorrere. I file di dati e i documenti vengono anche richiesti in porzioni MAX_IO_CHUNK in un File; i contenuti multimediali e qualsiasi file oltre 64 MB restano solo come url (getFile() li recupera su richiesta), quindi una clip da 300 MB non sta mai in un array JS.
Descritto dal suo gruppo, File condivisi con l'app o aperti con essa, e non da solo.
Esempio
Meccanismo interno dietro a AppMint.getOpenedFile(): restituisce una porzione dei byte di un file condiviso o aperto, che l'helper unisce in un File. Le pagine dovrebbero usare l'API pubblica: f.file, f.getFile() oppure f.url.
Restituisce: (sincrono) una stringa base64 di al massimo len byte a partire da offset (l'offset è passato come stringa, quindi funzionano anche file oltre i 2 GB); "" alla fine del file, per un token sconosciuto o rilasciato, o in caso di errore di lettura. Richiede: attiva Ricevi condivisioni da altre app quando crei l'app.
L'API pubblica (usa questa):
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'); });
});
}Cosa fa l'helper, mostrato solo per spiegare gli argomenti:
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);
}
}Lettura di un file condiviso molto grande senza porzioni: trasmetti f.url e mostra l'avanzamento (qui contando le righe di 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);
}Note: len è limitato tra 1 byte e 4 MB. Per i file multimediali, trasmettere f.url costa molto meno che richiedere le porzioni.
openedFileCollected bridge#
window.WebToApk.openedFileCollected(token: String)
Interno a AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La pagina richiede, invece di ricevere: un file aperto all'avvio a freddo resta in attesa finché non viene richiesto, quindi una SPA che si monta in ritardo lo riceve comunque. I metadati di ogni file includono `kind` (image | video | audio | document | data) e `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servito dalla copia nella cache con supporto agli intervalli di byte, così un video condiviso viene trasmesso in <video src> e si può scorrere. I file di dati e i documenti vengono anche richiesti in porzioni MAX_IO_CHUNK in un File; i contenuti multimediali e qualsiasi file oltre 64 MB restano solo come url (getFile() li recupera su richiesta), quindi una clip da 300 MB non sta mai in un array JS.
Descritto dal suo gruppo, File condivisi con l'app o aperti con essa, e non da solo.
Esempio
Meccanismo interno dietro a AppMint.getOpenedFile(): segna un file condiviso o aperto come raccolto, così non viene più proposto come «nuovo» ma la sua copia nella cache viene mantenuta. Le pagine dovrebbero usare l'API pubblica; non c'è motivo di chiamarlo direttamente.
Restituisce: niente. Richiede: attiva Ricevi condivisioni da altre app quando crei l'app.
L'API pubblica (usa questa): ogni file viene raccolto una volta e consegnato alla tua pagina:
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 file raccolto all'avvio è sia nell'elenco sia annunciato dall'evento, quindi gestiscilo usando token come chiave per elaborare ogni file una sola volta:
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 file che non è stato possibile leggere viene comunque raccolto; viene segnalato una sola volta tramite appmint:fileopenerror invece di 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);
});Note: Chiamarlo tu stesso nasconderebbe un file a AppMint.getOpenedFile() e all'evento appmint:fileopen senza consegnarlo a nessuno. Per liberare la copia nella cache, usa AppMint.releaseOpenedFile(f).
openedFileMaxChunk bridge#
window.WebToApk.openedFileMaxChunk(): Int
Interno a AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La pagina richiede, invece di ricevere: un file aperto all'avvio a freddo resta in attesa finché non viene richiesto, quindi una SPA che si monta in ritardo lo riceve comunque. I metadati di ogni file includono `kind` (image | video | audio | document | data) e `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servito dalla copia nella cache con supporto agli intervalli di byte, così un video condiviso viene trasmesso in <video src> e si può scorrere. I file di dati e i documenti vengono anche richiesti in porzioni MAX_IO_CHUNK in un File; i contenuti multimediali e qualsiasi file oltre 64 MB restano solo come url (getFile() li recupera su richiesta), quindi una clip da 300 MB non sta mai in un array JS.
Descritto dal suo gruppo, File condivisi con l'app o aperti con essa, e non da solo.
Esempio
Meccanismo interno dietro a AppMint.getOpenedFile(): la porzione più grande che openedFileChunk restituisce in una chiamata (4 MB). Le pagine dovrebbero usare l'API pubblica, che legge già in porzioni di questa dimensione.
Restituisce: (sincrono) un numero di byte (4194304). Richiede: attiva Ricevi condivisioni da altre app quando crei l'app.
L'API pubblica (usa questa):
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 chiamata grezza, se ti interessa solo il numero:
if (window.WebToApk && typeof WebToApk.openedFileMaxChunk === 'function') {
console.log('slice size', WebToApk.openedFileMaxChunk());
}Stima di quante chiamate al bridge farà l'helper per ogni file in attesa, quando analizzi un'importazione 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)');
});
}Note: Stesso limite di getMaxIoChunkSize() per il bridge di accesso ai file.
openedFileRelease bridge#
window.WebToApk.openedFileRelease(token: String)
Interno a AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). La pagina richiede, invece di ricevere: un file aperto all'avvio a freddo resta in attesa finché non viene richiesto, quindi una SPA che si monta in ritardo lo riceve comunque. I metadati di ogni file includono `kind` (image | video | audio | document | data) e `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, servito dalla copia nella cache con supporto agli intervalli di byte, così un video condiviso viene trasmesso in <video src> e si può scorrere. I file di dati e i documenti vengono anche richiesti in porzioni MAX_IO_CHUNK in un File; i contenuti multimediali e qualsiasi file oltre 64 MB restano solo come url (getFile() li recupera su richiesta), quindi una clip da 300 MB non sta mai in un array JS.
Descritto dal suo gruppo, File condivisi con l'app o aperti con essa, e non da solo.
Esempio
Meccanismo interno dietro a AppMint.releaseOpenedFile(f): elimina la copia nella cache di un file condiviso o aperto, tramite token. Le pagine dovrebbero usare l'API pubblica.
Restituisce: niente. Richiede: attiva Ricevi condivisioni da altre app quando crei l'app.
L'API pubblica (usa questa):
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)
}L'utente scarta un file condiviso senza importarlo: libera subito la copia nella cache:
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 file che l'app non può usare viene rilasciato appena viene rifiutato:
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
});Note: Dopo il rilascio, f.url restituisce un errore e openedFileChunk restituisce "" per quel token. Un oggetto File che hai già non è interessato.
openedFilesPending bridge#
window.WebToApk.openedFilesPending(): String
Una pagina esterna non ottiene nulla: i file restano in attesa per la pagina dell'app.
Esempio
Meccanismo interno dietro a AppMint.getOpenedFile() / AppMint.getOpenedFiles(): elenca i file condivisi o aperti che la pagina non ha ancora raccolto (solo metadati, nessun byte). Le pagine dovrebbero usare l'API pubblica.
Restituisce: (sincrono) una stringa JSON: un array di {token, name, mimeType, size, isText, kind, url}. [] quando non c'è nulla in attesa. Richiede: attiva Ricevi condivisioni da altre app quando crei l'app (senza, l'elenco è sempre vuoto e gli helper AppMint non vengono installati).
L'API pubblica (usa questa):
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 per il debug: dai un'occhiata a ciò che è in attesa senza raccoglierlo:
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; }));
}Debug di un file che non è mai arrivato alla pagina: guarda cosa è ancora in attesa e se arriverà come f.file o solo come 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)' : '');
});
}Note: L'helper li raccoglie automaticamente dopo load e ogni volta che arriva un nuovo file, quindi una pagina raramente vede qualcosa qui. Una volta raccolto, un file esce da questo elenco ma resta leggibile finché non viene rilasciato.