Crea APK gratis

API del bridge JavaScript

File: uno alla volta

Il selettore di sistema e la lettura/scrittura binaria a blocchi, per file troppo grandi per stare in una stringa.

AppMint.downloadFile helper#

AppMint.downloadFile(data, fileName, mimeType?)

Esempio

Salva un file creato dalla tua pagina (da una stringa base64, un URL data:, un URL blob: o un Blob) con il nome file che scegli. Si apre il foglio «Salva con nome…» di Android con quel nome già inserito.

Restituisce: (sincrono) true quando il salvataggio è stato passato ad Android (false solo per un URL data: che non è riuscita a interpretare). Genera TypeError se data non è un Blob né una stringa. L'utente sceglie comunque la cartella e può annullare. Richiede: niente: è sempre presente nell'app.

Una stringa base64 più un tipo MIME:

function exportCsv(rows) {
  const csv = rows.map(function (r) { return r.join(','); }).join('\n');
  const b64 = btoa(unescape(encodeURIComponent(csv)));          // UTF-8 safe
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(b64, 'Ledger_Q3.csv', 'text/csv');
  } else {
    const a = document.createElement('a');                       // a normal browser
    a.href = 'data:text/csv;base64,' + b64;
    a.download = 'Ledger_Q3.csv';
    a.click();
  }
}

Un URL data: (il tipo MIME viene dall'URL) o un Blob:

const png = document.querySelector('canvas').toDataURL('image/png');
AppMint.downloadFile(png, 'Chart.png');

const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
AppMint.downloadFile(blob, 'Backup.json');                        // same as AppMint.saveBlob

Un PDF in base64 che il tuo server restituisce in JSON; a un nome senza estensione viene aggiunto .pdf in base al tipo MIME:

async function downloadInvoice(id) {
  const res = await fetch('/api/invoices/' + id + '?format=base64');
  const body = await res.json();                                  // { base64: 'JVBERi0x…' }
  if (!window.AppMint || typeof AppMint.downloadFile !== 'function') {
    location.href = 'data:application/pdf;base64,' + body.base64;
    return;
  }
  AppMint.downloadFile(body.base64, 'Invoice_' + id, 'application/pdf');   // saved as Invoice_<id>.pdf
}

Un URL blob: che hai già creato per un'anteprima a schermo:

// earlier: preview.src = URL.createObjectURL(editedImageBlob);
document.getElementById('save-photo').addEventListener('click', function () {
  const preview = document.getElementById('preview');
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(preview.src, 'Edited_Photo.jpg');
  }
});

Note: Viene usata solo l'ultima parte di fileName (niente cartelle). A un nome senza estensione ne viene assegnata una in base al tipo MIME. Il terzo argomento viene usato solo per una normale stringa base64. Se il telefono non ha affatto la schermata «Salva con nome…», il file viene scritto direttamente in Download. Funziona anche il tuo normale codice <a download>: vedi downloads.

AppMint.saveBlob helper#

AppMint.saveBlob(blob, fileName)

Esempio

Salva un Blob (o File) con il nome che scegli. Si apre il foglio «Salva con nome…» di Android con quel nome già inserito.

Restituisce: (sincrono) true quando il salvataggio è stato passato ad Android. Genera TypeError se il primo argomento non è un Blob. Richiede: niente: è sempre presente nell'app.

async function saveReport() {
  const blob = await makePdfBlob();                    // e.g. jsPDF: doc.output('blob')
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Site_Report_2026-08.pdf');
    return;
  }
  const url = URL.createObjectURL(blob);               // a normal browser
  const a = document.createElement('a');
  a.href = url;
  a.download = 'Site_Report_2026-08.pdf';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
}

Un File porta con sé il proprio nome, quindi il nome si può omettere:

const file = new File(['name,score\n'], 'Scores.csv', { type: 'text/csv' });
AppMint.saveBlob(file);            // saved as Scores.csv

Un file dal tuo server che richiede un'intestazione di accesso, salvato con un nome a tua scelta:

async function saveStatement(month, token) {
  const res = await fetch('/api/statements/' + month, {
    headers: { Authorization: 'Bearer ' + token }
  });
  if (!res.ok) { alert('Could not fetch the statement (' + res.status + ')'); return; }
  const blob = await res.blob();
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Statement_' + month + '.pdf');
  }
}

Più file raccolti in un unico ZIP (qui con JSZip) e salvati in un colpo solo:

async function exportAll(notes) {
  const zip = new JSZip();
  notes.forEach(function (n) { zip.file(n.title + '.txt', n.body); });
  zip.file('index.json', JSON.stringify(notes.map(function (n) { return n.title; })));
  const blob = await zip.generateAsync({ type: 'blob' });
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Notes_' + new Date().toISOString().slice(0, 10) + '.zip');
  }
}

Note: Il Blob viene letto in memoria e inviato ad Android in un'unica soluzione, quindi blob molto grandi (centinaia di MB) possono esaurire la memoria. L'utente sceglie la cartella e può annullare; nulla dice alla pagina quale dei due casi si è verificato.

AppMint.setDownloadName helper#

AppMint.setDownloadName(url, fileName)

Esempio

Dà a un URL blob: o data: il nome con cui il file deve essere salvato, prima che il tuo codice (o una libreria) avvii il download.

Restituisce: niente. Richiede: niente: è sempre presente nell'app.

Utile quando una libreria scarica un URL che hai creato prima e non puoi passarle un nome:

const blob = new Blob([csvText], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
  AppMint.setDownloadName(url, 'Invoice_0042.csv');
}
location.href = url;           // saved as Invoice_0042.csv, not "download.csv"

Un PDF che la tua pagina apre con window.open va nel visualizzatore PDF del telefono; dagli prima un nome, così non si chiama «download.pdf»:

function previewPdf(pdfBlob, invoiceNo) {
  const url = URL.createObjectURL(pdfBlob);
  if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
    AppMint.setDownloadName(url, 'Invoice_' + invoiceNo + '.pdf');
  }
  window.open(url, '_blank');
}

Un URL data: funziona allo stesso modo, ad esempio l'immagine di un grafico a cui una libreria rimanda senza un attributo download:

const dataUrl = chart.toBase64Image();         // e.g. Chart.js: 'data:image/png;base64,…'
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
  AppMint.setDownloadName(dataUrl, 'Sales_by_month.png');
}
const link = document.getElementById('chart-link');
link.href = dataUrl;                           // a tap on this link saves Sales_by_month.png
link.textContent = 'Save chart';

Note: Il nome viene ricordato anche dopo URL.revokeObjectURL(url), perché alcune librerie revocano prima che il salvataggio sia finito. Raramente ti serve: <a download="name">, URL.createObjectURL(file) (un File ha un nome) e saveAs(blob, name) di FileSaver vengono riconosciuti automaticamente. Il nome 'download' viene ignorato e trattato come «nessun nome».

WebToApkFS.maxChunkSize helper#

WebToApkFS.maxChunkSize()

Esempio

Ti dice il massimo di byte che una chiamata readBytes o writeBytes può spostare (4 MB), così puoi dimensionare il tuo ciclo a blocchi.

Restituisce: (sincrono) un numero di byte, oppure 0 quando il metodo del bridge manca. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel Passaggio 4 (Accesso) quando crei l'app (è ciò che inietta WebToApkFS).

function copyFile(fromUri, toPath) {
  if (!window.WebToApkFS) return { ok: false, error: 'not in the app' };
  const step = WebToApkFS.maxChunkSize() || 1048576;   // fall back to 1 MB pieces
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    if (r.bytesRead > 0) {
      const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
      if (!w.ok) return w;
    }
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}

Mostra l'avanzamento della lettura: la dimensione del blocco ti dice quanti passaggi richiede un file:

function readWithProgress(uri, onProgress) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return info;
  const step = WebToApkFS.maxChunkSize() || 1048576;
  const steps = Math.max(1, Math.ceil(info.size / step));
  for (let i = 0; i < steps; i++) {
    const r = WebToApkFS.readBytes(uri, i * step, step);
    if (!r.ok) return r;
    onProgress(Math.round(((i + 1) / steps) * 100), r.base64);
  }
  return { ok: true, size: info.size };
}

Aggiungi un grande array di byte a un file nella cartella autorizzata, un pezzo consentito alla volta:

function appendBytes(path, bytes) {                    // bytes: a Uint8Array
  const step = WebToApkFS.maxChunkSize();
  if (!step) return { ok: false, error: 'not in the app' };
  for (let at = 0; at < bytes.length; at += step) {
    const piece = bytes.subarray(at, at + step);
    let bin = '';
    for (let i = 0; i < piece.length; i += 0x8000) bin += String.fromCharCode.apply(null, piece.subarray(i, i + 0x8000));
    const w = WebToApkFS.writeBytes(path, btoa(bin), 0, 'append');   // 'append' ignores the offset
    if (!w.ok) return w;
  }
  return { ok: true };
}

Note: Un blocco più piccolo mantiene la pagina più reattiva (ogni chiamata è sincrona); il limite indica solo quanto può essere grande una chiamata.

WebToApkFS.pickFile helper#

WebToApkFS.pickFile(mimeFilter)

Esempio

Apre il selettore di file di Android perché l'utente scelga UN file; la tua pagina può poi leggere e scrivere i byte reali di quel file, anche dopo il riavvio dell'app.

Restituisce: Promise<{ok, uri, name, size, mime, error}>. In caso di successo ok è true e uri è un indirizzo content:// da passare a stat, readBytes, writeBytes. In caso di fallimento ok è false e error è 'cancelled', 'disabled', 'no-picker', 'busy' (un selettore è già aperto per una chiamata precedente, che mantiene la propria risposta), 'foreign_origin' o 'unsupported' (nessuna app). Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

async function openOneFile() {
  if (!window.WebToApkFS) { alert('Open this page in the app to pick a file.'); return; }
  const f = await WebToApkFS.pickFile('application/pdf');   // '' = any file, 'image/*' = pictures
  if (!f.ok) {
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  console.log(f.name, f.size + ' bytes', f.mime);
  localStorage.setItem('lastFile', f.uri);   // the grant survives a restart
}

Riusa più tardi l'uri salvato, senza chiedere all'utente una seconda volta:

const uri = localStorage.getItem('lastFile');
if (uri && window.WebToApkFS) {
  const info = WebToApkFS.stat(uri);
  if (info.ok) console.log('still here:', info.name);
  else console.log('file is gone or access was removed:', info.error);
}

Il filtro accetta UN solo modello, quindi scegli in modo ampio e controlla tu il nome:

async function importTable() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');        // CSV and TSV files are both text/*
  if (!f.ok) {
    if (f.error === 'busy') return;                     // a picker is already open
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  if (!/\.(csv|tsv)$/i.test(f.name)) {
    WebToApkFS.releaseFile(f.uri);
    alert(f.name + ' is not a CSV or TSV file.');
    return;
  }
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) showTable(atob(r.base64), f.name.toLowerCase().endsWith('.tsv') ? '\t' : ',');
  WebToApkFS.releaseFile(f.uri);
}

Note: Il filtro è un unico modello di tipo MIME ('image/*', 'text/csv'), non un elenco. Il selettore chiede l'accesso in lettura E scrittura e lo mantiene. Android limita quanti file un'app può conservare in questo modo, quindi chiama WebToApkFS.releaseFile(uri) quando hai finito con un file. Per un'intera cartella usa invece WebToApkFS.requestAccess(). WebToApkFS viene installato quando è selezionato Accesso nativo alle cartelle o Chiavi sicure; le sue chiamate funzionano solo con Accesso nativo alle cartelle (WebToApkFS.isAvailable()).

WebToApkFS.readAll helper#

WebToApkFS.readAll(pathOrUri)

Esempio

Legge un intero file in un'unica stringa base64. Esegue per te un ciclo su readBytes, 4 MB alla volta.

Restituisce: (sincrono) {ok:true, base64, size}, oppure l'{ok:false, error} del primo blocco che fallisce. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Carica un'immagine scelta in un <img>:

async function showPicture() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('image/*');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  document.getElementById('photo').src = 'data:' + f.mime + ';base64,' + r.base64;
}

Leggi un file di testo UTF-8 (funziona con qualsiasi lingua, non solo l'inglese):

function readUtf8(pathOrUri) {
  const r = WebToApkFS.readAll(pathOrUri);
  if (!r.ok) throw new Error(r.error);
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  return new TextDecoder('utf-8').decode(bytes);
}

Mostra il checksum SHA-256 di un file, per esempio per confrontare un download con quello pubblicato:

async function checksum() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
  const hex = Array.from(hash, function (b) { return b.toString(16).padStart(2, '0'); }).join('');
  console.log(f.name, r.size + ' bytes', hex);
}

Note: L'intero file finisce in memoria (come byte e poi come base64) e la chiamata blocca la pagina mentre è in corso. Usala per file che stanno comodamente in memoria; per file grandi ripeti WebToApkFS.readBytes e gestisci ogni blocco. Per testo semplice dentro la cartella autorizzata, WebToApkFS.readText(path) è più semplice.

WebToApkFS.readBytes helper#

WebToApkFS.readBytes(pathOrUri, offset, length)

Esempio

Legge i byte reali di un file, a partire da una posizione in byte, come base64. Sicuro per immagini, PDF, ZIP: la decodifica del testo non cambia nulla.

Restituisce: (sincrono) {ok:true, base64, bytesRead, offset, size, eof} oppure {ok:false, error} ('not-found', 'disabled', 'cannot-open', 'offset past end of file (N bytes)', 'foreign_origin', 'unsupported'). Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Leggi i primi 64 KB, per esempio per controllare l'intestazione di un file:

async function checkPng() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('image/*');
  if (!f.ok) return;
  const r = WebToApkFS.readBytes(f.uri, 0, 65536);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const head = atob(r.base64).slice(1, 4);
  console.log(head === 'PNG' ? 'a PNG' : 'not a PNG', '- file is', r.size, 'bytes');
}

Leggi un file grande pezzo per pezzo, così non deve mai stare tutto in memoria:

function eachChunk(uri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(uri, offset, step);
    if (!r.ok) return r;                 // stop on the first error
    onChunk(r.base64, offset);
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}

Leggi solo la fine di un file, per esempio il tag di 128 byte alla fine di un MP3:

function readMp3Title(uri) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok || info.size < 128) return null;
  const r = WebToApkFS.readBytes(uri, info.size - 128, 128);
  if (!r.ok) return null;
  const tail = atob(r.base64);
  if (tail.slice(0, 3) !== 'TAG') return null;          // no old-style tag in this file
  return tail.slice(3, 33).replace(/\0+$/, '').trim();  // bytes 3-32 hold the title
}

Note: Una chiamata legge al massimo WebToApkFS.maxChunkSize() byte (4 MB); length 0 significa «quanto consente una chiamata». Ripeti con bytesRead e fermati con eof. La chiamata è sincrona, quindi usa blocchi moderati sul thread dell'interfaccia. Per file piccoli WebToApkFS.readAll(uri) fa il ciclo per te.

WebToApkFS.readSealed helper#

WebToApkFS.readSealed(pathOrUri, alias)

Esempio

Legge un file scritto da WebToApkFS.writeSealed e lo decifra con la stessa chiave del keystore.

Restituisce: (sincrono) {ok:true, base64} (i byte originali), oppure {ok:false, error}: 'keystore-disabled', 'corrupt-sealed-file', un codice del keystore ('no-such-key' dopo l'eliminazione della chiave, 'auth-required', 'key-invalidated') o un errore di file ('not-found' …). Richiede: seleziona Accesso nativo alle cartelle (SAF) e Chiavi sicure (crittografia) nel Passaggio 4 (Accesso) quando crei l'app.

function loadSecret() {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return null;
  const out = WebToApkFS.readSealed('secret.bin', 'vault');
  if (out.ok) {
    const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
    return new TextDecoder().decode(bytes);
  }
  if (out.error === 'auth-required') {
    alert('Unlock your phone, then try again.');
  } else if (out.error === 'no-such-key') {
    alert('This note was deleted.');
  } else {
    alert('Could not open the note: ' + out.error);
  }
  return null;
}

Conserva le impostazioni dell'app in un file JSON protetto, con valori predefiniti prima del primo salvataggio:

function loadSettings() {
  const defaults = { theme: 'light', pinHint: '' };
  const out = WebToApkFS.readSealed('settings.sealed', 'settings');
  if (!out.ok) {
    if (out.error === 'not-found') return defaults;      // nothing saved yet
    throw new Error('Settings could not be opened: ' + out.error);
  }
  const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
  return Object.assign(defaults, JSON.parse(new TextDecoder().decode(bytes)));
}

Sposta un file protetto su una nuova chiave, poi distruggi la vecchia chiave:

function rekey(path, oldAlias, newAlias) {
  const plain = WebToApkFS.readSealed(path, oldAlias);
  if (!plain.ok) return plain;
  if (!AppMintKeystore.hasKey(newAlias)) {
    const k = AppMintKeystore.generateKey(newAlias, {});
    if (!k.ok) return k;
  }
  const w = WebToApkFS.writeSealed(path, plain.base64, newAlias);
  if (!w.ok) return w;
  AppMintKeystore.deleteKey(oldAlias);                  // only after the new copy is written
  return { ok: true };
}

Note: Fallisce per sempre dopo che AppMintKeystore.deleteKey(alias) è stato eseguito: è proprio questo lo scopo. L'intero file viene letto e decifrato in memoria, in modo sincrono; tieni piccoli i file sigillati (note, chiavi, token).

WebToApkFS.releaseFile helper#

WebToApkFS.releaseFile(uri)

Esempio

Restituisce l'accesso a lungo termine ottenuto dalla tua app quando l'utente ha scelto un file con WebToApkFS.pickFile.

Restituisce: (sincrono) true quando l'autorizzazione è stata rilasciata, false altrimenti (nessuna autorizzazione, URI non valido, non siamo nell'app). Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

async function importOnce() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/csv');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) importCsv(atob(r.base64));
  WebToApkFS.releaseFile(f.uri);       // done with it: do not keep the grant
}

Quando l'utente sceglie un nuovo file predefinito, restituisci l'autorizzazione di quello vecchio:

async function chooseDefaultFile() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const old = localStorage.getItem('defaultFile');
  if (old && old !== f.uri) WebToApkFS.releaseFile(old);
  localStorage.setItem('defaultFile', f.uri);
}

Un pulsante «Dimentica file recenti» che rilascia tutti i file ricordati dall'app:

function forgetRecentFiles() {
  const recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  let released = 0;
  for (const uri of recent) {
    if (WebToApkFS.releaseFile(uri)) released++;       // false: already gone or never granted
  }
  localStorage.removeItem('recentFiles');
  console.log('released', released, 'of', recent.length);
}

Note: Android limita a quanti file scelti un'app può mantenere l'accesso. Un'app che sceglie molti file (un importatore, un visualizzatore) dovrebbe rilasciare ciascuno al termine. Dopo il rilascio, stat / readBytes / writeBytes su quell'URI rispondono not-found. Non elimina il file.

WebToApkFS.stat helper#

WebToApkFS.stat(pathOrUri)

Esempio

Ti dice nome, dimensione, tipo e data di un file senza leggerne il contenuto.

Restituisce: (sincrono) {ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}, oppure {ok:false, error} con error = 'not-found', 'disabled', 'foreign_origin' o 'unsupported'. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

async function showInfo() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const info = WebToApkFS.stat(f.uri);
  if (!info.ok) { alert('Cannot read file info: ' + info.error); return; }
  console.log(info.name, info.size + ' bytes', info.mime,
              new Date(info.lastModified).toLocaleString(),
              info.canWrite ? 'writable' : 'read only');
}

Funziona anche un percorso all'interno della cartella concessa dall'utente con WebToApkFS.requestAccess():

const info = WebToApkFS.stat('photos/cover.jpg');
if (info.ok && !info.isDirectory) console.log('cover is', info.size, 'bytes');

Reimporta un file ricordato solo quando è cambiato dall'ultima importazione:

function importIfChanged(uri) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return 'gone: ' + info.error;
  const seen = JSON.parse(localStorage.getItem('lastImport') || '{}');
  if (seen.size === info.size && seen.lastModified === info.lastModified) return 'unchanged';
  const r = WebToApkFS.readAll(uri);
  if (!r.ok) return 'read failed: ' + r.error;
  importData(atob(r.base64));
  localStorage.setItem('lastImport', JSON.stringify({ size: info.size, lastModified: info.lastModified }));
  return 'imported';
}

Note: pathOrUri è un URI content:// ottenuto da pickFile() oppure un percorso relativo alla cartella autorizzata. Un URI che la tua app non ha mai ricevuto restituisce not-found. lastModified è in millisecondi dal 1970 (0 quando il provider non lo indica).

WebToApkFS.writeAll helper#

WebToApkFS.writeAll(pathOrUri, base64)

Esempio

Sostituisce l'intero contenuto di un file con i tuoi byte. Divide i dati in pezzi da 4 MB per te: il primo pezzo tronca il file, gli altri vengono inseriti di seguito.

Restituisce: (sincrono) {ok:true, bytesWritten}, oppure l'{ok:false, error} del primo pezzo che fallisce. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Salva testo UTF-8 (in qualsiasi lingua) in un file scelto dall'utente:

function bytesToBase64(bytes) {
  let s = '';
  for (let i = 0; i < bytes.length; i += 0x8000) {
    s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  }
  return btoa(s);
}

async function saveNote(text) {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/plain');
  if (!f.ok) return;
  const r = WebToApkFS.writeAll(f.uri, bytesToBase64(new TextEncoder().encode(text)));
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes');
  else alert('Could not save: ' + r.error);
}

Scrivi l'immagine di un canvas nella cartella autorizzata (il file viene creato se manca):

const b64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
const r = WebToApkFS.writeAll('drawings/sketch.png', b64);
if (!r.ok) alert('Could not save: ' + r.error);

Salva un backup JSON nella cartella concessa, chiedendo prima una cartella se non ce n'è, poi conferma la sua dimensione sul disco:

async function backup(data) {
  if (!window.WebToApkFS || !WebToApkFS.isAvailable()) return;
  if (!WebToApkFS.hasAccess()) {
    const a = await WebToApkFS.requestAccess();
    if (!a.success) return;
  }
  const bytes = new TextEncoder().encode(JSON.stringify(data));
  let bin = '';
  for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  const r = WebToApkFS.writeAll('backup.json', btoa(bin));
  if (!r.ok) { alert('Backup failed: ' + r.error); return; }
  const info = WebToApkFS.stat('backup.json');
  console.log('backup is', info.ok ? info.size : '?', 'bytes');
}

Note: L'intero input resta in memoria mentre viene scritto e la chiamata blocca la pagina. Se un pezzo successivo fallisce, il file contiene già i pezzi precedenti: controlla ok e avvisa l'utente. Per tenere segreto un file, usa invece WebToApkFS.writeSealed.

WebToApkFS.writeBytes helper#

WebToApkFS.writeBytes(pathOrUri, base64, offset, mode)

Esempio

Scrive byte reali in un file e ti dice quanti sono arrivati a destinazione. Può modificare byte nel mezzo, sostituire l'intero file o aggiungere in fondo.

Restituisce: (sincrono) {ok, bytesWritten, offset, size}, dove size viene riletto dal file dopo che i dati sono stati scritti su disco, oppure {ok:false, error}. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Patch: cambia 4 byte alla posizione 100, mantiene il resto del file:

const r = WebToApkFS.writeBytes(fileUri, btoa('ABCD'), 100, 'patch');
if (r.ok) console.log('file is now', r.size, 'bytes');
else alert('Write failed: ' + r.error);

Truncate: sostituisce l'intero file (l'offset viene ignorato):

const csv = 'name,score\nAsha,12\n';
const r = WebToApkFS.writeBytes('exports/scores.csv', btoa(csv), 0, 'truncate');
if (!r.ok) alert('Could not save: ' + r.error);

Append: aggiunge in fondo (l'offset viene ignorato):

const line = new Date().toISOString() + ' opened\n';
WebToApkFS.writeBytes('log.txt', btoa(line), 0, 'append');

Note: mode è 'patch' (predefinito), 'truncate' o 'append'. Al massimo 4 MB per chiamata (WebToApkFS.maxChunkSize()); per dati più grandi usa WebToApkFS.writeAll. Un percorso dentro la cartella autorizzata viene creato se non esiste; un URI content:// scelto deve essere uno scelto dall'utente. btoa accetta solo testo Latin-1: per altro testo codificalo prima in byte (vedi WebToApkFS.writeAll). Errori: disabled, foreign_origin (la pagina a schermo non è quella dell'app), bad mode: x, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

WebToApkFS.writeSealed helper#

WebToApkFS.writeSealed(pathOrUri, base64, alias)

Esempio

Scrive un file cifrato con una chiave hardware di AppMintKeystore. Eliminare quella chiave in seguito rende il file illeggibile per chiunque: l'unica vera «eliminazione sicura» su un telefono.

Restituisce: (sincrono) {ok:true, bytesWritten} (dimensione cifrata su disco), oppure {ok:false, error}: 'keystore-disabled', un codice del keystore come 'no-such-key', 'auth-required', 'key-invalidated' o un errore del file. Richiede: seleziona Accesso nativo alle cartelle (SAF) e Chiavi sicure (crittografia) nel passaggio 4 (Accesso) quando crei l'app.

function saveSecret(text) {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) {
    alert('Secure storage is not available in this build.');
    return false;
  }
  if (!AppMintKeystore.hasKey('vault')) {
    const k = AppMintKeystore.generateKey('vault', {});
    if (!k.ok) { alert('Could not create key: ' + k.error); return false; }
  }
  const bytes = new TextEncoder().encode(text);
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  const r = WebToApkFS.writeSealed('secret.bin', btoa(bin), 'vault');
  if (!r.ok) alert('Could not save: ' + r.error);
  return r.ok;
}

Eliminalo davvero: distruggi la chiave, poi rimuovi il file:

AppMintKeystore.deleteKey('vault');   // secret.bin can never be decrypted again
WebToApkFS.delete('secret.bin');

Proteggi una foto scelta dall'utente, come byte grezzi, nella cartella a cui ha dato accesso:

async function sealPhoto(file) {                  // a File from <input type="file" accept="image/*">
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return false;
  if (!WebToApkFS.hasAccess()) {
    const g = await WebToApkFS.requestAccess();
    if (!g.success) return false;
  }
  if (!AppMintKeystore.hasKey('vault')) AppMintKeystore.generateKey('vault', {});
  const dataUrl = await new Promise(function (resolve, reject) {
    const fr = new FileReader();
    fr.onload = function () { resolve(fr.result); };
    fr.onerror = reject;
    fr.readAsDataURL(file);
  });
  const b64 = dataUrl.slice(dataUrl.indexOf(',') + 1);
  const r = WebToApkFS.writeSealed('photo-' + Date.now() + '.sealed', b64, 'vault');
  if (!r.ok) alert('Not saved: ' + r.error);
  return r.ok;
}

Una chiave per un diario che funziona solo per 5 minuti dopo lo sblocco del telefono, con ogni errore spiegato:

if (!AppMintKeystore.hasKey('diary')) {
  AppMintKeystore.generateKey('diary', { requireAuth: true, authValiditySeconds: 300 });
}

function saveDiary(entry) {
  const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(entry))));   // UTF-8 safe
  const r = WebToApkFS.writeSealed('diary.bin', b64, 'diary');
  if (r.ok) return true;
  if (r.error === 'auth-required') showMessage('Unlock your phone again, then tap Save.');
  else if (r.error === 'key-invalidated') showMessage('Your screen lock changed, so this key no longer works.');
  else showMessage('Could not save: ' + r.error);
  return false;
}

Note: Non «sovrascrivere con zeri» per cancellare: la memoria flash del telefono conserva i vecchi blocchi. Il formato del file è un elenco di pezzi AES-GCM con prefisso di lunghezza; rileggilo solo con WebToApkFS.readSealed e lo stesso alias. Una chiave creata con {requireAuth:true} risponde auth-required finché l'utente non ha sbloccato il telefono di recente.

downloads standard web#

<a download> and Blob downloads

Esempio

Il normale codice web di download funziona nell'app, come in Chrome. Una WebView ignora da sola i download blob: e data:; l'app li intercetta e salva il file nella cartella Download del telefono con il tuo nome di file, poi mostra "Scaricato <nome> · Apri" e una notifica "Download completato".

Restituisce: niente alla pagina. Richiede: niente da attivare. Anche i download da indirizzi web (https://…) vanno nella cartella Download (un PDF o un'immagine chiede prima «Visualizza o Scarica»), così come i file inclusi nell'app (<a href="manual.pdf" download>); su Android 9 e versioni precedenti l'app chiede prima l'autorizzazione all'archiviazione. I file grandi non sono un problema: il file viene trasmesso all'app a pezzi.

<a download> con un Blob, il caso più comune:

<button id="save">Download report</button>
<script>
document.getElementById('save').addEventListener('click', function () {
  const blob = new Blob(['Name,Total\nAsha,120\n'], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'Report_2026-08.csv';     // this is the name the user sees
  document.body.appendChild(a);
  a.click();                             // caught by the app, saved to Downloads
  a.remove();
});
</script>

Funzionano anche un URL data:, FileSaver.js e navigator.msSaveBlob:

const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = 'Drawing.png';
a.click();

if (typeof saveAs === 'function') saveAs(blob, 'Backup.json');   // FileSaver.js
if (navigator.msSaveBlob) navigator.msSaveBlob(blob, 'Backup.json');

window.open di un URL blob: o data: significa "mostralo", non "salvalo": un PDF si apre nel visualizzatore PDF dell'app, gli altri tipi nell'app del telefono per quel tipo:

const pdfUrl = URL.createObjectURL(pdfBlob);
const w = window.open(pdfUrl);    // never null in the app, so no "popup blocked" message

Note: Senza un nome il file viene salvato come download.<ext>; un secondo file con lo stesso nome diventa name (1).ext. Un oggetto File porta con sé il proprio nome (URL.createObjectURL(file)). <a href="/files/x" download="Name.pdf"> sul tuo sito mantiene Name.pdf (Chrome ignora l'attributo sui link di altri siti, e così fa l'app). Se nessuna app sul telefono riesce a mostrare un file window.open, viene salvato in Download. Per lasciare che l'utente scelga la cartella, usa showSaveFilePicker(). Per salvare senza un elemento <a>, usa AppMint.saveBlob(blob, name) oppure AppMint.downloadFile(base64, name, mime).

input-file standard web#

<input type="file"> — camera, photos and files

Esempio

Un normale campo di input file funziona nell'app come in Chrome: apre il selettore foto, la fotocamera o il selettore di file, e la tua pagina riceve normali oggetti File.

Restituisce: l'evento change dell'input con input.files (vuoto quando l'utente annulla). Richiede: foto e file non richiedono nulla. Per capture (la fotocamera) seleziona Fotocamera nel passaggio 4 (Accesso) quando crei l'app; l'app chiede l'autorizzazione alla fotocamera la prima volta.

Foto: apre il selettore foto di Android (nessuna richiesta di autorizzazione; la pagina riceve solo ciò che l'utente ha scelto):

<input type="file" id="pics" accept="image/*" multiple>
<div id="preview"></div>
<script>
document.getElementById('pics').addEventListener('change', function (e) {
  const box = document.getElementById('preview');
  for (let i = 0; i < e.target.files.length; i++) {
    const img = document.createElement('img');
    img.src = URL.createObjectURL(e.target.files[i]);
    img.style.width = '100px';
    box.appendChild(img);
  }
});
</script>

Fotocamera: capture salta il selettore e apre subito la fotocamera:

<input type="file" id="snap" accept="image/*" capture="environment">
<input type="file" id="clip" accept="video/*" capture="environment">
<script>
document.getElementById('snap').addEventListener('change', function (e) {
  const photo = e.target.files[0];
  if (!photo) return;                        // user closed the camera
  const form = new FormData();
  form.append('photo', photo);
  fetch('/api/upload', { method: 'POST', body: form });
});
</script>

Qualsiasi file, o piĂą tipi (funzionano anche le estensioni):

<input type="file" accept=".pdf,.csv,application/json" multiple>
ScriviCosa si apre
accept="image/*" / "video/*" / "image/*,video/*"il selettore foto (foto, video o entrambi)
aggiungi multiplel'utente può sceglierne più di uno
aggiungi capture (con Fotocamera selezionata)direttamente la fotocamera: foto per image/*, video per video/*
accept="image/*,.pdf" oppure no acceptil selettore di file di sistema

Note: Senza Fotocamera selezionata, capture viene ignorato e si apre il selettore. Quando è selezionato Accesso nativo alle cartelle (SAF), l'app mostra invece il proprio menu «Seleziona origine upload»: Scatta foto / Registra video (se Fotocamera è selezionata), Scegli file e Carica cartella (invia ogni file di una cartella). Con Archiviazione selezionato su Android 12 e precedenti, scegliere un file che non è una foto può chiedere prima l'autorizzazione all'archiviazione. I telefoni senza selettore foto usano il selettore di file, che mostra le stesse foto.

showOpenFilePicker standard web#

showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access

Esempio

L'API standard File System Access funziona dentro l'app, sopra il selettore di documenti di Android: apri un file e salva DI NUOVO IN QUEL FILE, crea un file con la schermata Salva con nome, oppure ottieni un'intera cartella in cui leggere, creare ed eliminare file.

Restituisce: showOpenFilePicker(options) → Promise di un array di FileSystemFileHandle; showSaveFilePicker(options) → un FileSystemFileHandle; showDirectoryPicker(options) → un FileSystemDirectoryHandle. Un selettore annullato rifiuta con AbortError; l'assenza di un gesto dell'utente rifiuta con SecurityError; un secondo selettore mentre uno è aperto rifiuta con NotAllowedError. Richiede: niente da attivare e nessuna autorizzazione: sceglie l'utente.

let handle = null;

document.getElementById('open').addEventListener('click', async () => {
  try {
    [handle] = await showOpenFilePicker({
      types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.md'] } }]
    });
    const file = await handle.getFile();
    editor.value = await file.text();
    title.textContent = file.name;
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});

document.getElementById('save').addEventListener('click', async () => {
  if (!handle) handle = await showSaveFilePicker({ suggestedName: 'note.txt' });
  const w = await handle.createWritable();   // writes go to a copy…
  await w.write(editor.value);
  await w.close();                           // …that replaces the file only here
});

document.getElementById('export').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  const out = await dir.getFileHandle('export.csv', { create: true });
  const w = await out.createWritable();
  await w.write(csvText);
  await w.close();
  for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);
});

Scegli più foto insieme, partendo dalla cartella Immagini:

document.getElementById('add-photos').addEventListener('click', async () => {
  try {
    const handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ description: 'Images', accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
    for (const h of handles) {
      const img = document.createElement('img');
      img.src = URL.createObjectURL(await h.getFile());
      gallery.appendChild(img);
    }
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});

Aggiungi una riga alla fine di un file di log senza riscrivere ciò che c'è già:

async function appendLine(handle, line) {      // handle from showOpenFilePicker / showSaveFilePicker
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);
  await w.write(line + '\n');
  await w.close();
}

Scegli una cartella di backup UNA SOLA VOLTA e salva al suo interno automaticamente a ogni avvio successivo: conserva l'handle in IndexedDB, esattamente come in Chrome. L'app mantiene l'autorizzazione della cartella tra un riavvio e l'altro, quindi queryPermission() risponde 'granted' e il backup non richiede nessun tocco e nessun selettore:

// Tiny IndexedDB helpers (idb-keyval's get/set work the same way).
function db() {
  return new Promise((ok, fail) => {
    const r = indexedDB.open('backup-settings', 1);
    r.onupgradeneeded = () => r.result.createObjectStore('kv');
    r.onsuccess = () => ok(r.result);
    r.onerror = () => fail(r.error);
  });
}
async function kvGet(key) {
  const d = await db();
  return new Promise((ok, fail) => {
    const r = d.transaction('kv').objectStore('kv').get(key);
    r.onsuccess = () => ok(r.result);
    r.onerror = () => fail(r.error);
  });
}
async function kvSet(key, value) {
  const d = await db();
  return new Promise((ok, fail) => {
    const t = d.transaction('kv', 'readwrite');
    t.objectStore('kv').put(value, key);
    t.oncomplete = () => ok();
    t.onerror = () => fail(t.error);
  });
}

// "Choose backup folder" button (the picker needs a tap).
document.getElementById('choose-folder').addEventListener('click', async () => {
  try {
    const dir = await showDirectoryPicker({ mode: 'readwrite', startIn: 'documents' });
    await kvSet('backupDir', dir);
    status.textContent = 'Backups go to ' + dir.name;
  } catch (e) {
    if (e.name !== 'AbortError') status.textContent = 'Could not choose: ' + e.message;
  }
});

// Called after every workout, and on a timer: no tap needed once the folder is chosen.
async function autoBackup(data) {
  const dir = await kvGet('backupDir');
  if (!dir) return 'no folder chosen yet';
  if ((await dir.queryPermission({ mode: 'readwrite' })) !== 'granted') {
    return 'folder access was removed: tap "Choose backup folder" again';
  }
  const file = await dir.getFileHandle('backup-' + new Date().toISOString().slice(0, 10) + '.json', { create: true });
  const w = await file.createWritable();
  await w.write(JSON.stringify(data));
  await w.close();
  return 'saved ' + file.name;
}

Se l'autorizzazione è stata rimossa (l'utente ha cancellato l'accesso dell'app o ha eliminato la cartella), queryPermission() risponde 'prompt'. Con un tocco su un pulsante, requestPermission() riapre il selettore di cartelle NELLA cartella in questione; scegliere la stessa cartella risponde 'granted', qualsiasi altra scelta (o l'annullamento) 'denied':

document.getElementById('reconnect').addEventListener('click', async () => {
  const dir = await kvGet('backupDir');
  if (dir && (await dir.requestPermission({ mode: 'readwrite' })) === 'granted') {
    status.textContent = 'Backups go to ' + dir.name + ' again';
  } else {
    status.textContent = 'Choose a backup folder';
  }
});

Pulisci una cartella scelta dall'utente: elimina i vecchi file .tmp e le sottocartelle chiamate cache:

document.getElementById('clean').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  let removed = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file' && entry.name.endsWith('.tmp')) {
      await dir.removeEntry(entry.name);
      removed++;
    } else if (entry.kind === 'directory' && entry.name === 'cache') {
      await dir.removeEntry(entry.name, { recursive: true });
      removed++;
    }
  }
  alert(removed + ' item(s) removed');
});

Note: createWritable() segue il modello a file di scambio della specifica: abort() (o la pagina che scompare) lascia intatto l'originale; { keepExistingData: true } parte dal contenuto attuale; write({type:'seek'|'truncate'|'write', …}), seek() e truncate() funzionano. Un file che la sua app offre in sola lettura risponde queryPermission({mode:'readwrite'}) con 'denied' e createWritable() rifiuta con NoModificationAllowedError. getFile() trasmette il documento, quindi i file grandi non sono un problema. Gli handle possono essere salvati in IndexedDB (anche dentro oggetti e array) e riutilizzati dopo il riavvio dell'app: l'app mantiene l'autorizzazione di Android per ogni cartella e file restituito da un selettore (i 64 più recenti; il più vecchio viene rilasciato per primo). queryPermission() risponde 'granted' finché l'autorizzazione è attiva e 'prompt' quando non c'è più; requestPermission() richiede allora un tocco e riapre il selettore nella cartella dell'handle stesso. Le app create prima del 2026-10-03 perdevano l'handle al riavvio: ricrea l'app e l'utente sceglie di nuovo la cartella una volta. types diventano il filtro file del selettore. Solo la pagina di primo livello ha i selettori. Trasporti interni: __fsPick (con risposta tramite l'evento appmint:fs), __fsFile, __fsList, __fsChild, __fsRemove, __fsPermission, __fsWritableOpen/Write/Truncate/Close/Abort. Le app create con l'AI controllano can('fileHandles').

__dlAbort bridge#

window.WebToApk.__dlAbort(token: String)

Scarta il download [token] (la lettura della pagina è fallita o ha rinunciato).

Esempio

Documentato insieme a WebToApk.__dlBegin - l'esempio lì mostra anche questo.

Trasporto interno dietro a <a download>, ai download di Blob, a window.open(blobUrl) e a AppMint.saveBlob / AppMint.downloadFile: il file viene trasmesso dalla pagina all'app a porzioni, così un'esportazione grande non diventa mai un'unica stringa enorme in memoria. Le pagine usano il codice standard di download; non chiamare mai direttamente questi metodi, perché i metodi __ possono cambiare senza preavviso.

Restituisce: __dlBegin(name, type, size) restituisce un token (oppure "" quando il telefono non ha spazio); __dlChunk(token, base64) restituisce false se una porzione non è stata scritta; __dlEnd(token, action) completa l'operazione (save = Download, open = mostralo, share, copy). Richiede: niente.

Usa l'API pubblica:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

Esporta un disegno da un canvas seguendo lo stesso percorso:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

Salva un backup JSON che l'utente può conservare, con un messaggio a operazione conclusa:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

Note: Implementato nella shell dell'app (ShellDownloads.kt).

__dlBegin bridge#

window.WebToApk.__dlBegin(name: String, type: String, size: Double): String

Apre un download di [size] byte chiamato [name] ([type] = il suo tipo MIME) che la pagina trasmetterà con __dlChunk. Restituisce un token, oppure "" quando non c'è spazio.

Esempio

Trasporto interno dietro a <a download>, ai download di Blob, a window.open(blobUrl) e a AppMint.saveBlob / AppMint.downloadFile: il file viene trasmesso dalla pagina all'app a porzioni, così un'esportazione grande non diventa mai un'unica stringa enorme in memoria. Le pagine usano il codice standard di download; non chiamare mai direttamente questi metodi, perché i metodi __ possono cambiare senza preavviso.

Restituisce: __dlBegin(name, type, size) restituisce un token (oppure "" quando il telefono non ha spazio); __dlChunk(token, base64) restituisce false se una porzione non è stata scritta; __dlEnd(token, action) completa l'operazione (save = Download, open = mostralo, share, copy). Richiede: niente.

Usa l'API pubblica:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

Esporta un disegno da un canvas seguendo lo stesso percorso:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

Salva un backup JSON che l'utente può conservare, con un messaggio a operazione conclusa:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

Note: Implementato nella shell dell'app (ShellDownloads.kt).

__dlChunk bridge#

window.WebToApk.__dlChunk(token: String, base64: String): Boolean

Una porzione base64 del download [token]; false quando il trasferimento è fallito.

Esempio

Documentato insieme a WebToApk.__dlBegin - l'esempio lì mostra anche questo.

Trasporto interno dietro a <a download>, ai download di Blob, a window.open(blobUrl) e a AppMint.saveBlob / AppMint.downloadFile: il file viene trasmesso dalla pagina all'app a porzioni, così un'esportazione grande non diventa mai un'unica stringa enorme in memoria. Le pagine usano il codice standard di download; non chiamare mai direttamente questi metodi, perché i metodi __ possono cambiare senza preavviso.

Restituisce: __dlBegin(name, type, size) restituisce un token (oppure "" quando il telefono non ha spazio); __dlChunk(token, base64) restituisce false se una porzione non è stata scritta; __dlEnd(token, action) completa l'operazione (save = Download, open = mostralo, share, copy). Richiede: niente.

Usa l'API pubblica:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

Esporta un disegno da un canvas seguendo lo stesso percorso:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

Salva un backup JSON che l'utente può conservare, con un messaggio a operazione conclusa:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

Note: Implementato nella shell dell'app (ShellDownloads.kt).

__dlEnd bridge#

window.WebToApk.__dlEnd(token: String, action: String): Boolean

Completa il download [token] ed esegue l'azione [action]: «save» (in Download, con la barra «Scaricato · Apri» e la notifica), «open» (visualizzatore PDF o l'app del telefono per quel tipo), «share» (foglio di condivisione) o «copy» (immagine negli appunti).

Esempio

Documentato insieme a WebToApk.__dlBegin - l'esempio lì mostra anche questo.

Trasporto interno dietro a <a download>, ai download di Blob, a window.open(blobUrl) e a AppMint.saveBlob / AppMint.downloadFile: il file viene trasmesso dalla pagina all'app a porzioni, così un'esportazione grande non diventa mai un'unica stringa enorme in memoria. Le pagine usano il codice standard di download; non chiamare mai direttamente questi metodi, perché i metodi __ possono cambiare senza preavviso.

Restituisce: __dlBegin(name, type, size) restituisce un token (oppure "" quando il telefono non ha spazio); __dlChunk(token, base64) restituisce false se una porzione non è stata scritta; __dlEnd(token, action) completa l'operazione (save = Download, open = mostralo, share, copy). Richiede: niente.

Usa l'API pubblica:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

Esporta un disegno da un canvas seguendo lo stesso percorso:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

Salva un backup JSON che l'utente può conservare, con un messaggio a operazione conclusa:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

Note: Implementato nella shell dell'app (ShellDownloads.kt).

__fsChild bridge#

window.WebToApk.__fsChild(uri: String, name: String, kind: String, create: Boolean): String

getFileHandle / getDirectoryHandle (con `create`).

Esempio

Trasporto interno dietro a getFileHandle(name, {create}) e getDirectoryHandle(name, {create}). Le pagine usano l'API standard; non chiamarlo mai direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok, kind, name, uri, canWrite} oppure {ok:false, error} (NotFoundError, TypeMismatchError, NoModificationAllowedError). Richiede: una cartella ottenuta da showDirectoryPicker().

Usa l'API pubblica:

const dir = await showDirectoryPicker();
const photos = await dir.getDirectoryHandle('photos', { create: true });
const file = await photos.getFileHandle('list.json', { create: true });

Controlla se un file esiste già nella cartella prima di crearlo:

async function exists(dir, name) {
  try {
    await dir.getFileHandle(name);                     // no {create} = look up only
    return true;
  } catch (e) {
    if (e.name === 'NotFoundError') return false;
    if (e.name === 'TypeMismatchError') return true;   // a folder already has that name
    throw e;
  }
}

Salva le impostazioni dell'app come JSON in una sottocartella «config», creando entrambe se serve:

async function saveSettings(root, settings) {
  const config = await root.getDirectoryHandle('config', { create: true });
  const file = await config.getFileHandle('settings.json', { create: true });
  const w = await file.createWritable();
  await w.write(JSON.stringify(settings, null, 2));
  await w.close();
}
// root = await showDirectoryPicker()  (from a button tap)

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsFile bridge#

window.WebToApk.__fsFile(uri: String): String

getFile(): `{ok, name, type, size, lastModified, url}` (il corpo viene trasmesso dall'url).

Esempio

Trasporto interno dietro FileSystemFileHandle.getFile(). Le pagine usano l'API standard; non chiamare mai questo direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok, name, type, size, lastModified, url} (il contenuto viene trasmesso da url, l'origine dell'app stessa, con supporto agli intervalli) oppure {ok:false, error} (NotFoundError, TypeMismatchError, foreign_origin). Richiede: un handle ottenuto da uno dei selettori.

Usa l'API pubblica:

const [handle] = await showOpenFilePicker();
const file = await handle.getFile();
console.log(file.name, file.size, await file.text());

Mostra nella pagina un'immagine scelta senza leggerla in una stringa:

document.querySelector('#choose').addEventListener('click', async function () {
  const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': [] } }] });
  const file = await handle.getFile();                    // a real File, streamed from the app
  const img = document.querySelector('#preview');
  if (img.src) URL.revokeObjectURL(img.src);
  img.src = URL.createObjectURL(file);
  img.alt = file.name + ' (' + file.type + ')';
});

Rileggi un file aperto in precedenza e accorgiti quando è cambiato o è stato eliminato:

async function reloadIfChanged(handle, lastSeen) {
  try {
    const file = await handle.getFile();
    if (file.lastModified === lastSeen) return null;      // unchanged
    return { data: JSON.parse(await file.text()), lastModified: file.lastModified };
  } catch (e) {
    if (e.name === 'NotFoundError') alert(handle.name + ' was moved or deleted.');
    else throw e;
    return null;
  }
}

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsList bridge#

window.WebToApk.__fsList(uri: String): String

Le voci di un handle di cartella: `{ok, entries:[{kind, name, uri, canWrite}]}`.

Esempio

Trasporto interno dietro all'iterazione di un FileSystemDirectoryHandle (entries(), keys(), values(), for await). Le pagine usano l'API standard; non chiamarlo mai direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok, entries:[{kind, name, uri, canWrite}]} oppure {ok:false, error}. Richiede: una cartella ottenuta da showDirectoryPicker().

Usa l'API pubblica:

const dir = await showDirectoryPicker();
for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);

Conta i file in una cartella e in tutte le sue sottocartelle:

async function countFiles(dir) {
  let n = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file') n++;
    else n += await countFiles(entry);                 // entry is a FileSystemDirectoryHandle
  }
  return n;
}
document.querySelector('#scan').addEventListener('click', async function () {
  const dir = await showDirectoryPicker();
  console.log(dir.name, 'holds', await countFiles(dir), 'files');
});

Crea un elenco ordinato delle foto in una cartella scelta dall'utente:

document.querySelector('#open-album').addEventListener('click', async function () {
  const dir = await showDirectoryPicker({ startIn: 'pictures' });
  const names = [];
  for await (const name of dir.keys()) {
    if (/\.(jpe?g|png|webp)$/i.test(name)) names.push(name);
  }
  names.sort(function (a, b) { return a.localeCompare(b); });
  renderAlbum(dir, names);                             // dir.getFileHandle(name) opens each one
});

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsPermission bridge#

window.WebToApk.__fsPermission(rootUri: String, uri: String): String

queryPermission() / requestPermission(): `{ok, state:'granted'|'prompt'}` per un handle la cui radice scelta è [rootUri]: «granted» finché l'app detiene l'autorizzazione di quella scelta, anche tra un avvio e l'altro (l'autorizzazione è persistente; un handle torna da IndexedDB).

Esempio

Trasporto interno dietro a FileSystemHandle.queryPermission() e requestPermission(). Le pagine usano l'API standard; non chiamarlo mai direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok:true, state:'granted'} finché l'app detiene l'autorizzazione della cartella o del file scelto da cui proviene l'handle (mantenuta tra i riavvii), {ok:true, state:'prompt'} quando non c'è più, oppure {ok:false, error:'foreign_origin'}. Un file di sola lettura risponde 'denied' per {mode:'readwrite'} nella pagina, senza fare una chiamata. Richiede: un handle ottenuto da uno dei selettori, anche riletto da IndexedDB.

Usa l'API pubblica:

const dir = await kvGet('backupDir');                 // a handle stored in IndexedDB earlier
if ((await dir.queryPermission({ mode: 'readwrite' })) === 'granted') {
  const f = await dir.getFileHandle('backup.json', { create: true });
  const w = await f.createWritable();
  await w.write(JSON.stringify(state));
  await w.close();
}

Chiedi di nuovo con un tocco quando l'autorizzazione non c'è più: il selettore di cartelle si apre in quella cartella e solo la stessa cartella concede l'accesso:

document.getElementById('reconnect').addEventListener('click', async () => {
  const dir = await kvGet('backupDir');
  const state = await dir.requestPermission({ mode: 'readwrite' });   // 'granted' | 'denied'
  status.textContent = state === 'granted' ? 'Backups resumed' : 'Choose a backup folder';
});

Note: Implementato nella shell dell'app (FileSystemAccess.kt). Vedi showOpenFilePicker per l'intera API File System Access.

__fsPick bridge#

window.WebToApk.__fsPick(requestId: String, kind: String, optionsJson: String): String

Apre il selettore [kind] (open | save | directory); gli handle arrivano come `appmint:fs`.

Esempio

Trasporto interno dietro a showOpenFilePicker(), showSaveFilePicker() e showDirectoryPicker(). Le pagine usano l'API standard; non chiamarlo mai direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: {ok:true} quando il selettore è sullo schermo, oppure {ok:false, error} (NotAllowedError quando ce n'è già uno aperto, foreign_origin). Gli handle arrivano con l'evento appmint:fs {requestId, handles:[{kind, name, uri, canWrite}]} oppure {requestId, error:'cancelled'}. Richiede: niente da attivare.

Usa l'API pubblica:

const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': ['.png', '.jpg'] } }] });
const file = await handle.getFile();

Salva un report con un nome suggerito, da un tocco su un pulsante (i selettori richiedono un gesto dell'utente):

document.querySelector('#export').addEventListener('click', async function () {
  try {
    const handle = await showSaveFilePicker({
      suggestedName: 'report.csv',
      types: [{ description: 'CSV', accept: { 'text/csv': ['.csv'] } }]
    });
    const w = await handle.createWritable();
    await w.write(buildCsv());
    await w.close();
  } catch (e) {
    if (e.name !== 'AbortError') alert('Export failed: ' + e.message);   // AbortError = cancelled
  }
});

Lascia che l'utente scelga più foto insieme, partendo dalla cartella Immagini:

document.querySelector('#add-photos').addEventListener('click', async function () {
  let handles;
  try {
    handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
  } catch (e) { return; }                                 // cancelled or already open
  for (const h of handles) addToGallery(await h.getFile());
});

Note: Implementato nella shell dell'app (FileSystemAccess.kt). Lo stesso codice della pagina funziona senza modifiche in Chrome desktop.

__fsRemove bridge#

window.WebToApk.__fsRemove(uri: String, name: String, recursive: Boolean): String

removeEntry(name, {recursive}).

Esempio

Trasporto interno dietro FileSystemDirectoryHandle.removeEntry(name, {recursive}). Le pagine usano l'API standard; non chiamare mai questo direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok:true} oppure {ok:false, error} (NotFoundError, InvalidModificationError per una cartella non vuota senza recursive). Richiede: una cartella ottenuta da showDirectoryPicker().

Usa l'API pubblica:

const dir = await showDirectoryPicker();
await dir.removeEntry('old-drafts', { recursive: true });

Elimina un file e considera «già sparito» come operazione riuscita:

async function deleteFile(dir, name) {
  try {
    await dir.removeEntry(name);
  } catch (e) {
    if (e.name !== 'NotFoundError') throw e;   // NotFoundError: nothing left to delete
  }
}

Rimuovi una cartella, chiedendo prima solo quando contiene ancora qualcosa:

async function removeFolder(dir, name) {
  try {
    await dir.removeEntry(name);                          // succeeds for an empty folder
  } catch (e) {
    if (e.name !== 'InvalidModificationError') throw e;
    if (!confirm('"' + name + '" is not empty. Delete it and everything inside?')) return false;
    await dir.removeEntry(name, { recursive: true });
  }
  return true;
}

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsWritableAbort bridge#

window.WebToApk.__fsWritableAbort(token: String)

abort(): lo scambio viene scartato; il documento resta intatto.

Esempio

Trasporto interno dietro a FileSystemWritableFileStream.abort(): la copia di scambio viene scartata e il file resta com'era. Le pagine usano l'API standard; non chiamarlo mai direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: niente. Richiede: un flusso scrivibile aperto.

Usa l'API pubblica:

const w = await handle.createWritable();
try { await w.write(await buildExport()); await w.close(); }
catch (e) { await w.abort(); }

Lascia che l'utente annulli un'esportazione lunga; nel file non arriva nulla di scritto a metà:

let cancelled = false;
document.getElementById('cancel').onclick = function () { cancelled = true; };

async function exportAll(handle, pages) {
  const w = await handle.createWritable();
  for (const page of pages) {
    if (cancelled) { await w.abort(); return 'cancelled'; }   // the file keeps its old content
    await w.write(await renderPage(page));
  }
  await w.close();
  return 'saved';
}

Scarica direttamente in un file con pipeTo; interrompere il download annulla la scrittura:

const ctl = new AbortController();
document.getElementById('stop').onclick = function () { ctl.abort(); };

async function downloadInto(handle, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error('HTTP ' + res.status);
  const w = await handle.createWritable();
  try {
    await res.body.pipeTo(w, { signal: ctl.signal });     // closes the file when the body ends
    return 'saved';
  } catch (e) {
    return e.name === 'AbortError' ? 'stopped, file unchanged' : 'failed: ' + e.message;
  }
}

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsWritableClose bridge#

window.WebToApk.__fsWritableClose(token: String): String

close(): la copia di scambio sostituisce il documento.

Esempio

Trasporto interno dietro a FileSystemWritableFileStream.close(): la copia di scambio sostituisce il file. Le pagine usano l'API standard; non chiamarlo mai direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok:true} o {ok:false, error} (NoModificationAllowedError, InvalidModificationError quando l'app del file non può accorciarlo). Richiede: un flusso scrivibile aperto.

Usa l'API pubblica:

const w = await handle.createWritable();
await w.write(json);
await w.close();   // nothing changes on disk before this

Spiega all'utente perché un salvataggio non è andato a buon fine (close() è il punto in cui può fallire):

async function saveText(handle, text) {
  const w = await handle.createWritable();
  await w.write(text);
  try {
    await w.close();
    return true;
  } catch (e) {
    if (e.name === 'InvalidModificationError') alert('This file\'s app cannot make it shorter. Save a copy instead.');
    else if (e.name === 'NoModificationAllowedError') alert('This file cannot be changed.');
    else alert('Save failed: ' + e.message);
    return false;
  }
}

Scrivi un file in più pezzi; chi lo legge vede ancora il vecchio contenuto finché l'unico close():

async function saveRows(handle, rows) {
  const w = await handle.createWritable();
  await w.write('id,name,total\n');
  for (const r of rows) await w.write(r.id + ',' + r.name + ',' + r.total + '\n');
  console.log('before close:', (await handle.getFile()).size, 'bytes (old file)');
  await w.close();
  console.log('after close:', (await handle.getFile()).size, 'bytes');
}

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsWritableOpen bridge#

window.WebToApk.__fsWritableOpen(uri: String, keepExisting: Boolean): String

createWritable(): un file di scambio, copiato dal file quando [keepExisting].

Esempio

Trasporto interno dietro FileSystemFileHandle.createWritable({keepExistingData}). Le pagine usano l'API standard; non chiamare mai questo direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok, token} per una copia di scambio privata, oppure {ok:false, error} (NoModificationAllowedError per un file di sola lettura). Richiede: un handle ottenuto da uno dei selettori.

Usa l'API pubblica:

const handle = await showSaveFilePicker({ suggestedName: 'report.csv' });
const w = await handle.createWritable({ keepExistingData: false });
await w.write('a,b\n1,2\n');
await w.close();

Aggiungi una riga alla fine di un file di log esistente, mantenendo quanto già contiene:

async function appendLine(handle, line) {
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                     // write after the old content
  await w.write(new Date().toISOString() + ' ' + line + '\n');
  await w.close();                                        // only now does the file change
}

Salva un canvas come PNG e lascia il file intatto se qualcosa va storto:

document.querySelector('#save-png').addEventListener('click', async function () {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (r) { document.querySelector('canvas').toBlob(r, 'image/png'); });
  const w = await handle.createWritable();
  try {
    await w.write(blob);
    await w.close();
  } catch (e) {
    try { await w.abort(); } catch (x) {}                 // nothing reaches the file before close()
    alert('Could not save: ' + e.message);
  }
});

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsWritableTruncate bridge#

window.WebToApk.__fsWritableTruncate(token: String, size: Double): String

truncate(size) su un flusso scrivibile.

Esempio

Trasporto interno dietro FileSystemWritableFileStream.truncate(size). Le pagine usano l'API standard; non chiamare mai questo direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok:true} o {ok:false, error}. Richiede: un flusso scrivibile aperto.

Usa l'API pubblica:

const w = await handle.createWritable({ keepExistingData: true });
await w.truncate(0);
await w.write('fresh start');
await w.close();

Tieni solo i primi 1.000 byte di un file e taglia tutto ciò che viene dopo:

async function keepFirstKilobyte(handle) {
  const w = await handle.createWritable({ keepExistingData: true });
  await w.truncate(1000);   // a file shorter than this grows instead (see the next example)
  await w.close();
}

Riserva spazio: troncare a una dimensione maggiore fa crescere il file e i nuovi byte vengono riletti come zeri:

async function makeEmptySlotFile(handle, slots, slotSize) {
  const w = await handle.createWritable();
  await w.truncate(slots * slotSize);                  // e.g. 100 slots of 256 bytes, all zero
  await w.write({ type: 'write', position: 0, data: new Uint8Array([1, 0, 0, 0]) });
  await w.close();
  console.log('file is now', (await handle.getFile()).size, 'bytes');
}

Note: Implementato nella shell dell'app (FileSystemAccess.kt).

__fsWritableWrite bridge#

window.WebToApk.__fsWritableWrite(token: String, position: Double, base64: String): String

Un blocco del write() di un flusso scrivibile, alla posizione [position].

Esempio

Trasporto interno dietro a FileSystemWritableFileStream.write(): un blocco a una posizione nella copia di scambio. Le pagine usano l'API standard; non chiamarlo mai direttamente: i metodi __ possono cambiare senza preavviso.

Restituisce: JSON {ok:true} o {ok:false, error}. Richiede: un flusso scrivibile aperto.

Usa l'API pubblica:

const w = await handle.createWritable({ keepExistingData: true });
await w.write({ type: 'write', position: 0, data: new Blob(['HEADER\n']) });
await w.close();

Aggiungi una riga alla fine di un file di log scelto dall'utente (un selettore di file richiede un tocco):

document.getElementById('addLine').addEventListener('click', async () => {
  const [handle] = await showOpenFilePicker({
    types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.log'] } }]
  });
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                   // start writing after the old content
  await w.write(new Date().toISOString() + ' checked in\n');
  await w.close();
});

Salva un disegno del canvas come nuovo PNG con «Salva con nome» (un Blob viene scritto come byte grezzi):

document.getElementById('saveDrawing').addEventListener('click', async () => {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ description: 'PNG image', accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (ok) { document.querySelector('canvas').toBlob(ok, 'image/png'); });
  const w = await handle.createWritable();
  await w.write(blob);
  await w.close();
});

Note: Implementato nella shell dell'app (FileSystemAccess.kt). I dati di grandi dimensioni vengono inviati a blocchi.

getMaxIoChunkSize bridge#

window.WebToApk.getMaxIoChunkSize(): Long

Il limite di byte per chiamata di [readFileBase64] / [writeFileBase64], utile per calcolare i blocchi.

Esempio

Il limite di byte per chiamata di readFileBase64 / writeFileBase64 (4 MB). Le pagine di solito usano WebToApkFS.maxChunkSize().

Restituisce: (sincrono) un numero (4194304). Richiede: niente per chiamarlo; i metodi di lettura/scrittura che descrive richiedono Accesso nativo alle cartelle (SAF) selezionato nel passaggio 4 (Accesso).

Il modo pubblico:

const step = window.WebToApkFS ? WebToApkFS.maxChunkSize() : 0;

La chiamata grezza:

let step = 1048576;
if (window.WebToApk && typeof WebToApk.getMaxIoChunkSize === 'function') {
  step = Number(WebToApk.getMaxIoChunkSize()) || step;
}
console.log('read/write up to', step, 'bytes per call');

Calcola quante chiamate al bridge richiederà un file, per dimensionare una barra di avanzamento prima di copiarlo:

function plannedSteps(fileUri) {
  const step = WebToApkFS.maxChunkSize();          // 0 when the helper is not available
  const info = WebToApkFS.stat(fileUri);
  if (!step || !info.ok) return 0;
  const steps = Math.max(1, Math.ceil(info.size / step));
  document.querySelector('#progress').max = steps;
  return steps;
}

Copia un file in un altro, un blocco della dimensione massima alla volta:

function copyFile(fromUri, toPath) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
    if (!w.ok) return w;
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, bytes: offset };
  }
}
// copyFile(pickedUri, 'photo-backup.jpg')  (a file in the granted folder)

Note: Una scrittura più grande di questo limite viene rifiutata con chunk too large (max N bytes); una lettura più grande viene ridotta a questa dimensione.

pickFile bridge#

window.WebToApk.pickFile(requestId: String, mimeFilter: String)

Apre il selettore di sistema per un singolo file (ACTION_OPEN_DOCUMENT). A differenza di [requestFolderAccess], concede l'accesso a un solo file, che è ciò che dovrebbe chiedere un editor o un distruttore di file; l'autorizzazione di lettura/scrittura è persistente, quindi l'URI continua a funzionare anche dopo che il processo è stato terminato. La risposta arriva su `WebToApkOnFilePickResult(success, fileJson, requestId)`; l'`error` di un fallimento è "cancelled", "disabled", "no-picker", "foreign_origin" o "busy" (un selettore è già aperto per una richiesta precedente, che mantiene la propria risposta). @param mimeFilter ad es. "image/\*", "application/pdf", oppure "" per qualsiasi file. (L'asterisco è preceduto da una barra rovesciata solo perché i commenti a blocco di Kotlin si annidano.)

Esempio

Apre il selettore di sistema per UN file. Le pagine di solito usano l'helper Promise WebToApkFS.pickFile(mimeFilter); questo metodo grezzo risponde tramite una callback.

Restituisce: per ora niente. Più tardi la shell chiama window.WebToApkOnFilePickResult(success, fileJson, requestId), dove fileJson è una stringa JSON {ok, uri, name, size, mime} in caso di successo oppure {ok:false, uri:'', error} con error = 'cancelled', 'disabled', 'no-picker', 'busy' o 'foreign_origin'. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Il modo pubblico (consigliato):

const f = await WebToApkFS.pickFile('image/*');
if (f.ok) console.log('picked', f.name, f.uri);

La chiamata grezza, con il tuo controllo requestId:

function pickRaw(mime) {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof WebToApk.pickFile !== 'function') { resolve(null); return; }
    const myId = 'pick_' + Date.now();
    window.WebToApkOnFilePickResult = function (success, fileJson, requestId) {
      if (requestId !== myId) return;
      const r = JSON.parse(fileJson);           // fileJson is a string
      resolve(success && r.ok ? r : null);      // null = cancelled or not allowed
    };
    WebToApk.pickFile(myId, mime || '');        // '' = any file
  });
}

Allega un PDF e mostra nome e dimensione, distinguendo un annullamento da un vero problema:

document.querySelector('#attach').addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('application/pdf');
  if (!f.ok) {
    if (f.error === 'cancelled' || f.error === 'busy') return;   // user closed it / one already open
    alert('Could not open the file picker: ' + f.error);         // 'disabled', 'no-picker', ...
    return;
  }
  document.querySelector('#attachment').textContent =
    f.name + ' (' + Math.round(f.size / 1024) + ' KB)';
  localStorage.setItem('attachmentUri', f.uri);   // the grant is kept across restarts
});

Apri un file di testo e carica il suo contenuto in un editor:

async function openTextFile() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);            // {ok, base64, size} or {ok:false, error}
  if (!r.ok) { alert('Could not read ' + f.name + ': ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  document.querySelector('#editor').value = new TextDecoder('utf-8').decode(bytes);
  document.querySelector('#editor').dataset.uri = f.uri;   // save back to the same file later
}

Note: Può essere aperta una sola selezione alla volta: una chiamata fatta mentre una è aperta riceve subito la risposta error:'busy', e quella aperta mantiene la propria risposta. La concessione viene mantenuta (lettura e scrittura) finché non chiami releaseFileAccess(uri). Il file non viene letto qui: usa readFileBase64 / WebToApkFS.readBytes con l' uri.

readFileBase64 bridge#

window.WebToApk.readFileBase64(pathOrUri: String, offset: Long, length: Long): String

Legge fino a [length] byte grezzi a partire da [offset] e li restituisce in base64. @param pathOrUri un URI `content://` da [pickFile], oppure un percorso relativo alla cartella concessa da [requestFolderAccess]. @param length byte da leggere; limitato a 4 MB per chiamata. 0 o negativo significa "quanto consente una chiamata". @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` dice a un ciclo a blocchi quando fermarsi senza dover confrontare con `size`.

Esempio

Legge byte grezzi da un file a partire da un offset in byte e li restituisce in base64. Le pagine di solito usano WebToApkFS.readBytes(pathOrUri, offset, length) (stesso risultato, già analizzato) oppure WebToApkFS.readAll(pathOrUri).

Restituisce: (sincrono) una stringa JSON {ok:true, base64, bytesRead, offset, size, eof} oppure {ok:false, error}. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Il modo pubblico:

const r = WebToApkFS.readBytes(fileUri, 0, 65536);
if (r.ok) console.log(r.bytesRead, 'bytes read, end of file:', r.eof);

La chiamata grezza: nota che il risultato è una stringa da elaborare con JSON.parse, non un semplice base64:

if (window.WebToApk && typeof WebToApk.readFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.readFileBase64(fileUri, 0, 0));   // length 0 = up to 4 MB
  if (r.ok) {
    const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
    console.log('first byte', bytes[0], 'of', r.size);
  } else {
    console.log('read failed:', r.error);
  }
}

Verifica che un file scelto sia davvero un PNG leggendo solo i suoi primi 8 byte:

function isPng(fileUri) {
  const r = WebToApkFS.readBytes(fileUri, 0, 8);
  if (!r.ok || r.bytesRead < 8) return false;
  const head = atob(r.base64);
  const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
  return sig.every(function (b, i) { return head.charCodeAt(i) === b; });
}

Leggi un file grande blocco per blocco con una barra di avanzamento, senza mai tenerlo tutto in memoria:

function scanInChunks(fileUri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fileUri, offset, step);
    if (!r.ok) throw new Error('read failed: ' + r.error);
    onChunk(r.base64);                                    // process this piece, then drop it
    offset += r.bytesRead;
    document.querySelector('#progress').value = r.size ? offset / r.size : 1;
    if (r.eof || r.bytesRead <= 0) break;
  }
  return offset;                                          // total bytes read
}

Note: pathOrUri è un URI content:// ottenuto da pickFile oppure un percorso relativo alla cartella ottenuta con requestFolderAccess. Al massimo 4 MB per chiamata (getMaxIoChunkSize()); ripeti con bytesRead finché eof. Errori: not-found, disabled, cannot-open, offset must be >= 0, offset past end of file (N bytes), foreign_origin (una pagina di un sito diverso da quello dell'app).

releaseFileAccess bridge#

window.WebToApk.releaseFileAccess(uriString: String): String

Rilascia un'autorizzazione persistente per un singolo file ottenuta con [pickFile]. Android limita quante autorizzazioni URI un'app può rendere persistenti, quindi un'app che sceglie molti file dovrebbe rilasciare quelli che ha finito di usare.

Esempio

Rilascia l'autorizzazione di lettura/scrittura mantenuta per un file scelto con pickFile. Le pagine di solito usano WebToApkFS.releaseFile(uri), che restituisce un vero valore booleano.

Restituisce: (sincrono) la stringa "true" oppure "false". Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app (altrimenti sempre "false").

Il modo pubblico:

if (window.WebToApkFS) WebToApkFS.releaseFile(fileUri);

La chiamata grezza:

if (window.WebToApk && typeof WebToApk.releaseFileAccess === 'function') {
  const released = WebToApk.releaseFileAccess(fileUri) === 'true';   // a string, not a boolean
  console.log(released ? 'access released' : 'nothing to release');
}

Mantieni un elenco di «file recenti» di al massimo 10 e rilascia l'autorizzazione di ogni file che ne esce:

function rememberRecent(uri) {
  let recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  recent = [uri].concat(recent.filter(function (u) { return u !== uri; }));
  while (recent.length > 10) {
    const dropped = recent.pop();
    if (window.WebToApkFS) WebToApkFS.releaseFile(dropped);   // Android caps kept grants
  }
  localStorage.setItem('recentFiles', JSON.stringify(recent));
}

Chiudi il documento corrente: rilascia la sua autorizzazione e dimentica l'URI salvato:

document.querySelector('#close-doc').addEventListener('click', function () {
  const uri = localStorage.getItem('openDocUri');
  if (uri && window.WebToApkFS) {
    const released = WebToApkFS.releaseFile(uri);   // true / false, already a boolean
    console.log(released ? 'grant released' : 'no grant was held for this file');
  }
  localStorage.removeItem('openDocUri');
  document.querySelector('#editor').value = '';
});

Note: Android limita quante concessioni di file un'app può mantenere, quindi rilascia i file che non ti servono più. Il file stesso non viene toccato.

statFile bridge#

window.WebToApk.statFile(pathOrUri: String): String

Metadati di un file o di una cartella senza leggerne nulla. Un percorso relativo indica un file o una sottocartella della cartella autorizzata; una cartella risponde `isDirectory: true` con `size` 0 e `mime` `vnd.android.document/directory`. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, oppure `{ok:false, error}` con `not-found`, `disabled` o `foreign_origin`.

Esempio

Legge i metadati di un file (nome, dimensione, tipo) senza leggerne i byte. Le pagine di solito usano WebToApkFS.stat(pathOrUri), che analizza il JSON per te.

Restituisce: (sincrono) una stringa JSON {ok, name, size, mime, lastModified, uri, isDirectory, canWrite}, oppure {ok:false, error} ('not-found', 'disabled', 'foreign_origin'). Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Il modo pubblico:

const info = WebToApkFS.stat(fileUri);
if (info.ok) console.log(info.name, info.size);

La chiamata grezza:

if (window.WebToApk && typeof WebToApk.statFile === 'function') {
  const info = JSON.parse(WebToApk.statFile(fileUri));   // or 'notes/today.txt' in the granted folder
  if (info.ok) console.log(info.name, info.size + ' bytes', info.mime, info.canWrite);
  else console.log('stat failed:', info.error);
}

Rifiuta un file troppo grande prima di leggerne anche un solo byte:

const MAX_BYTES = 20 * 1024 * 1024;
function canImport(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) { alert('File not available: ' + info.error); return false; }
  if (info.size > MAX_BYTES) {
    alert(info.name + ' is ' + Math.round(info.size / 1048576) + ' MB; the limit is 20 MB.');
    return false;
  }
  return true;
}

Cerca una sottocartella della cartella autorizzata e proponi «Salva qui» solo quando è scrivibile:

function checkExportFolder() {
  const info = WebToApkFS.stat('exports');          // a path relative to the granted folder
  const btn = document.querySelector('#save-here');
  if (!info.ok) {                                    // 'not-found' = the folder is not there yet
    btn.textContent = 'Create "exports" and save';
    return WebToApkFS.mkdir('exports');
  }
  btn.disabled = !(info.isDirectory && info.canWrite);
  console.log('exports last changed', new Date(info.lastModified).toLocaleString());
  return true;
}

Note: Accetta un URI content:// ottenuto da pickFile oppure un percorso relativo alla cartella autorizzata con requestFolderAccess. Risponde solo alle pagine dell'app o al suo sito web configurato; una pagina di qualsiasi altro sito ottiene foreign_origin.

writeFileBase64 bridge#

window.WebToApk.writeFileBase64(pathOrUri: String, base64: String, offset: Long, mode: String): String

Scrive byte grezzi e conferma quanti ne sono stati scritti. @param mode `"patch"` scrive a [offset] lasciando intatto il resto del file (ciò che serve a una sovrascrittura a più passaggi), `"truncate"` sostituisce il file, `"append"` ignora [offset] e scrive alla fine. @return JSON `{ok, bytesWritten, offset, size, error}` - `size` viene riletto dal file dopo un fsync, così chi chiama può verificare davvero la scrittura invece di fidarsi di un booleano.

Esempio

Scrive byte base64 in un file a un offset (patch), sull'intero file (truncate) o in fondo (append). Le pagine di solito usano WebToApkFS.writeBytes(pathOrUri, base64, offset, mode) oppure WebToApkFS.writeAll(pathOrUri, base64).

Restituisce: (sincrono) una stringa JSON {ok, bytesWritten, offset, size} (size riletta dopo un fsync) oppure {ok:false, error}. Richiede: seleziona Accesso nativo alle cartelle (SAF) nel passaggio 4 (Accesso) quando crei l'app.

Il modo pubblico:

const r = WebToApkFS.writeBytes(fileUri, btoa('hello'), 0, 'truncate');
if (r.ok) console.log('saved', r.bytesWritten, 'bytes');

La chiamata grezza:

if (window.WebToApk && typeof WebToApk.writeFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.writeFileBase64('notes/today.txt', btoa('Buy milk\n'), 0, 'append'));
  if (!r.ok) console.log('write failed:', r.error);
  else console.log('file is now', r.size, 'bytes');
}

Salva un disegno del canvas come PNG in un file scelto dall'utente, sostituendo ciò che c'era:

document.querySelector('#save').addEventListener('click', async function () {
  const f = await WebToApkFS.pickFile('image/png');
  if (!f.ok) return;
  const base64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
  const r = WebToApkFS.writeAll(f.uri, base64);   // truncates, then writes in 4 MB chunks
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes to', f.name);
  else alert('Save failed: ' + r.error);
});

Sovrascrivi un file con zeri prima di eliminarlo e conferma che ogni passaggio sia andato a buon fine:

function zeroFill(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) return false;
  const slice = 32768;
  for (let at = 0; at < info.size; at += slice) {
    const len = Math.min(slice, info.size - at);
    const zeros = btoa(String.fromCharCode.apply(null, new Uint8Array(len)));
    const w = WebToApkFS.writeBytes(fileUri, zeros, at, 'patch');   // the rest of the file is kept
    if (!w.ok || w.bytesWritten !== len) { console.log('write failed at', at, w.error); return false; }
  }
  return true;   // each call is fsync'ed before it answers
}

Note: La modalità è 'patch' (predefinita se vuota), 'truncate' o 'append'. Una chiamata accetta al massimo 4 MB di byte decodificati (getMaxIoChunkSize()). Un percorso relativo viene creato nella cartella concessa se manca. Errori: disabled, foreign_origin (la pagina sullo schermo non appartiene all'app), bad mode: x, offset must be >= 0, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

Generato dal runtime dell'app e dai suoi file di esempio a ogni build della documentazione. Leggilo in Markdown · Tutte le famiglie.

Verificato rispetto al bridge distribuito il 22/10/2026.