Criar APK grátis

API da ponte JavaScript

Arquivos: um arquivo por vez

O seletor do sistema e a leitura/gravação binária em blocos, para arquivos grandes demais para caber em uma string.

AppMint.downloadFile helper#

AppMint.downloadFile(data, fileName, mimeType?)

Exemplo

Salva um arquivo que sua página criou - a partir de uma string base64, uma URL data:, uma URL blob: ou um Blob - com o nome de arquivo que você escolher. A folha "Salvar como…" do Android abre com esse nome já preenchido.

Retorna: (síncrono) true quando o salvamento foi entregue ao Android (false só para uma URL data: que ele não conseguiu interpretar). Lança TypeError se data não for um Blob nem uma string. O usuário ainda escolhe a pasta e pode cancelar. Requer: nada - está sempre presente no app.

String Base64 mais um 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();
  }
}

Uma URL data: (o tipo MIME vem da URL) ou um 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

Um PDF em base64 que seu servidor retorna em JSON; um nome sem extensão recebe .pdf a partir do 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
}

Uma URL blob: que você já criou para uma pré-visualização na tela:

// 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');
  }
});

Observações: Só a última parte de fileName é usada (sem pastas). Um nome sem extensão recebe uma a partir do tipo MIME. O terceiro argumento só é usado para uma string base64 simples. Se o celular não tiver nenhuma tela "Salvar como…", o arquivo é gravado direto em Downloads. Seu código <a download> normal também funciona - veja downloads.

AppMint.saveBlob helper#

AppMint.saveBlob(blob, fileName)

Exemplo

Salva um Blob (ou File) com o nome que você escolher. A folha "Salvar como…" do Android abre com esse nome já preenchido.

Retorna: (síncrono) true quando o salvamento foi entregue ao Android. Lança TypeError quando o primeiro argumento não é um Blob. Requer: nada - está sempre presente no 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);
}

Um File traz o próprio nome, então o nome pode ser omitido:

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

Um arquivo do seu servidor que precisa de um cabeçalho de login, salvo com um nome que você escolhe:

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');
  }
}

Vários arquivos reunidos em um único ZIP (aqui com JSZip) e salvos de uma só vez:

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');
  }
}

Observações: O Blob é lido na memória e enviado ao Android de uma vez, então blobs muito grandes (centenas de MB) podem esgotar a memória. O usuário escolhe a pasta e pode cancelar; nada informa à página o que aconteceu.

AppMint.setDownloadName helper#

AppMint.setDownloadName(url, fileName)

Exemplo

Dá a uma URL blob: ou data: o nome de arquivo sob o qual ela deve ser salva, antes que seu código (ou uma biblioteca) dispare o download.

Retorna: nada. Requer: nada - está sempre presente no app.

Útil quando uma biblioteca baixa uma URL que você criou antes e você não consegue passar um nome a ela:

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"

Um PDF que sua página abre com window.open vai para o visualizador de PDF do celular; dê um nome antes para que não seja "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');
}

Uma URL data: funciona do mesmo jeito, por exemplo uma imagem de gráfico para a qual uma biblioteca aponta sem o atributo 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';

Observações: O nome é lembrado mesmo depois de URL.revokeObjectURL(url), porque algumas bibliotecas revogam antes de o salvamento terminar. Você raramente precisa disto: <a download="name">, URL.createObjectURL(file) (um File tem nome) e o saveAs(blob, name) do FileSaver são todos reconhecidos automaticamente. O nome 'download' é ignorado como "sem nome".

WebToApkFS.maxChunkSize helper#

WebToApkFS.maxChunkSize()

Exemplo

Informa o máximo de bytes que uma chamada readBytes ou writeBytes pode mover (4 MB), para você dimensionar seu loop de blocos.

Retorna: (síncrono) um número de bytes, ou 0 quando o método da bridge não existe. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar (é isso que injeta 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 };
  }
}

Mostre o progresso da leitura: o tamanho do bloco diz quantas etapas um arquivo leva:

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 };
}

Acrescente um grande array de bytes a um arquivo na pasta autorizada, uma parte permitida por vez:

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 };
}

Observações: Um bloco menor mantém a página mais responsiva (cada chamada é síncrona); o limite só diz o tamanho máximo de uma chamada.

WebToApkFS.pickFile helper#

WebToApkFS.pickFile(mimeFilter)

Exemplo

Abre o seletor de arquivos do Android para o usuário escolher UM arquivo; sua página pode então ler e gravar os bytes reais desse arquivo, mesmo depois que o app reiniciar.

Retorna: Promise<{ok, uri, name, size, mime, error}>. Em caso de sucesso, ok é true e uri é um endereço content:// que você passa para stat, readBytes, writeBytes. Em caso de falha, ok é false e error é 'cancelled', 'disabled', 'no-picker', 'busy' (já há um seletor aberto para uma chamada anterior, que mantém a própria resposta), 'foreign_origin' ou 'unsupported' (nenhum app). Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

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
}

Use o uri salvo de novo mais tarde, sem perguntar ao usuário uma segunda vez:

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);
}

O filtro aceita UM padrão, então escolha de forma ampla e confira o nome você mesmo:

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);
}

Observações: O filtro é um único padrão de tipo MIME ('image/*', 'text/csv'), não uma lista. O seletor pede acesso de leitura E gravação e o mantém. O Android limita quantos arquivos um app pode manter assim, então chame WebToApkFS.releaseFile(uri) quando terminar com um arquivo. Para uma pasta inteira use WebToApkFS.requestAccess(). WebToApkFS é instalado quando Acesso nativo a pastas ou Chaves seguras está marcado; suas chamadas só funcionam com Acesso nativo a pastas (WebToApkFS.isAvailable()).

WebToApkFS.readAll helper#

WebToApkFS.readAll(pathOrUri)

Exemplo

Lê um arquivo inteiro em uma única string base64. Ele percorre readBytes para você, 4 MB por vez.

Retorna: (síncrono) {ok:true, base64, size}, ou o {ok:false, error} do primeiro bloco que falhar. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

Carregue uma imagem escolhida em um <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;
}

Leia um arquivo de texto UTF-8 (funciona para qualquer idioma, não só inglês):

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);
}

Mostre o checksum SHA-256 de um arquivo, por exemplo para comparar um download com o publicado:

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);
}

Observações: O arquivo inteiro vai para a memória (como bytes e depois como base64), e a chamada bloqueia a página enquanto roda. Use para arquivos que cabem tranquilamente na memória; para arquivos grandes, repita WebToApkFS.readBytes e trate cada bloco. Para texto simples dentro da pasta autorizada, WebToApkFS.readText(path) é mais simples.

WebToApkFS.readBytes helper#

WebToApkFS.readBytes(pathOrUri, offset, length)

Exemplo

Lê os bytes reais de um arquivo, a partir de uma posição em bytes, como base64. Seguro para imagens, PDFs, ZIPs - nada é alterado por decodificação de texto.

Retorna: (síncrono) {ok:true, base64, bytesRead, offset, size, eof} ou {ok:false, error} ('not-found', 'disabled', 'cannot-open', 'offset past end of file (N bytes)', 'foreign_origin', 'unsupported'). Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

Leia os primeiros 64 KB, por exemplo para conferir o cabeçalho de um arquivo:

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');
}

Leia um arquivo grande pedaço por pedaço, para que ele nunca precise caber inteiro na memória:

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 };
  }
}

Leia só o final de um arquivo, por exemplo a tag de 128 bytes no fim de um 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
}

Observações: Uma chamada lê no máximo WebToApkFS.maxChunkSize() bytes (4 MB); length 0 significa "o máximo que uma chamada permitir". Repita com bytesRead e pare em eof. A chamada é síncrona, então mantenha os blocos moderados na thread da interface. Para arquivos pequenos, WebToApkFS.readAll(uri) faz o loop para você.

WebToApkFS.readSealed helper#

WebToApkFS.readSealed(pathOrUri, alias)

Exemplo

Lê um arquivo gravado por WebToApkFS.writeSealed e o descriptografa com a mesma chave do keystore.

Retorna: (síncrono) {ok:true, base64} (os bytes originais), ou {ok:false, error} — 'keystore-disabled', 'corrupt-sealed-file', um código do keystore ('no-such-key' depois que a chave foi excluída, 'auth-required', 'key-invalidated') ou um erro de arquivo ('not-found' …). Requer: marque Acesso nativo a pastas (SAF) e Chaves seguras (criptografia) na Etapa 4 (Acesso) ao compilar.

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;
}

Guarde as configurações do app em um arquivo JSON selado, com padrões antes do primeiro salvamento:

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)));
}

Mova um arquivo selado para uma nova chave e depois destrua a chave antiga:

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 };
}

Observações: Falha para sempre depois que AppMintKeystore.deleteKey(alias) foi executado — esse é o objetivo. O arquivo inteiro é lido e descriptografado na memória, de forma síncrona; mantenha os arquivos selados pequenos (notas, chaves, tokens).

WebToApkFS.releaseFile helper#

WebToApkFS.releaseFile(uri)

Exemplo

Devolve o acesso de longo prazo que seu app recebeu quando o usuário escolheu um arquivo com WebToApkFS.pickFile.

Retorna: (síncrono) true quando a permissão foi liberada, false caso contrário (sem permissão, URI inválida, fora do app). Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

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 o usuário escolher um novo arquivo padrão, devolva a permissão do antigo:

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);
}

Um botão "Esquecer arquivos recentes" que libera todos os arquivos que o app lembrava:

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);
}

Observações: O Android limita a quantos arquivos escolhidos um app pode manter acesso. Um app que escolhe muitos arquivos (um importador, um visualizador) deve liberar cada um quando terminar. Depois da liberação, stat / readBytes / writeBytes nessa URI respondem not-found. Isso não exclui o arquivo.

WebToApkFS.stat helper#

WebToApkFS.stat(pathOrUri)

Exemplo

Informa o nome, tamanho, tipo e data de um arquivo sem ler seu conteúdo.

Retorna: (síncrono) {ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}, ou {ok:false, error} com error = 'not-found', 'disabled', 'foreign_origin' ou 'unsupported'. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

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');
}

Um caminho dentro da pasta que o usuário concedeu com WebToApkFS.requestAccess() também funciona:

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

Reimporte um arquivo lembrado somente quando ele mudou desde a última importação:

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';
}

Observações: pathOrUri é uma URI content:// vinda de pickFile() ou um caminho relativo à pasta autorizada. Uma URI que seu app nunca recebeu retorna not-found. lastModified é em milissegundos desde 1970 (0 quando o provedor não informa).

WebToApkFS.writeAll helper#

WebToApkFS.writeAll(pathOrUri, base64)

Exemplo

Substitui todo o conteúdo de um arquivo pelos seus bytes. Ele divide os dados em partes de 4 MB para você: a primeira parte trunca o arquivo, as demais são aplicadas depois dela.

Retorna: (síncrono) {ok:true, bytesWritten}, ou o {ok:false, error} do primeiro trecho que falhar. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

Salve texto UTF-8 (qualquer idioma) de volta em um arquivo escolhido pelo usuário:

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);
}

Grave uma imagem de canvas na pasta autorizada (o arquivo é criado se não existir):

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);

Salve um backup JSON na pasta concedida, pedindo uma pasta primeiro se não houver, e depois confirme o tamanho dele no 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');
}

Observações: Toda a entrada fica na memória enquanto é gravada, e a chamada bloqueia a página. Se uma parte posterior falhar, o arquivo já contém as partes anteriores - confira ok e avise o usuário. Para manter um arquivo em segredo, use WebToApkFS.writeSealed.

WebToApkFS.writeBytes helper#

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

Exemplo

Grava bytes reais em um arquivo e informa quantos foram gravados. Pode alterar bytes no meio, substituir o arquivo inteiro ou acrescentar ao final.

Retorna: (síncrono) {ok, bytesWritten, offset, size} - size é relido do arquivo depois que os dados são gravados no disco - ou {ok:false, error}. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

Alterar - mude 4 bytes na posição 100, mantendo o resto do arquivo:

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);

Truncar - substitui o arquivo inteiro (o deslocamento é ignorado):

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);

Acrescentar - adiciona ao final (o deslocamento é ignorado):

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

Observações: mode é 'patch' (padrão), 'truncate' ou 'append'. No máximo 4 MB por chamada (WebToApkFS.maxChunkSize()); para dados maiores use WebToApkFS.writeAll. Um caminho dentro da pasta autorizada é criado se não existir; uma URI content:// escolhida precisa ser uma que o usuário escolheu. btoa só aceita texto Latin-1 - para outros textos, codifique em bytes primeiro (veja WebToApkFS.writeAll). Erros: disabled, foreign_origin (a página na tela não é a do próprio 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)

Exemplo

Grava um arquivo criptografado com uma chave protegida por hardware vinda de AppMintKeystore. Excluir essa chave depois torna o arquivo ilegível para todos - a única "exclusão segura" de verdade em um celular.

Retorna: (síncrono) {ok:true, bytesWritten} (tamanho criptografado no disco), ou {ok:false, error} - 'keystore-disabled', um código do keystore como 'no-such-key', 'auth-required', 'key-invalidated', ou um erro de arquivo. Requer: marque Acesso nativo a pastas (SAF) e Chaves seguras (criptografia) na Etapa 4 (Acesso) ao compilar.

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;
}

Exclua de verdade - destrua a chave e depois remova o arquivo:

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

Sele uma foto escolhida pelo usuário, como bytes brutos, na pasta que ele autorizou:

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;
}

Uma chave de diário que só funciona por 5 minutos depois que o celular foi desbloqueado, com cada erro explicado:

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;
}

Observações: Não "sobrescreva com zeros" para apagar: o armazenamento flash do celular mantém os blocos antigos. O formato do arquivo é uma lista de partes AES-GCM com prefixo de comprimento; leia de volta apenas com WebToApkFS.readSealed e o mesmo alias. Uma chave criada com {requireAuth:true} responde auth-required até que o usuário tenha desbloqueado o celular há pouco.

downloads padrão web#

<a download> and Blob downloads

Exemplo

Seu código web normal de download funciona no app, como no Chrome. Uma WebView ignora downloads blob: e data: por conta própria; o app os captura e salva o arquivo na pasta Downloads do telefone com o seu nome de arquivo, depois mostra "Baixado <nome> · Abrir" e uma notificação "Download concluído".

Retorna: nada para a página. Requer: nada para ativar. Downloads por endereço da web (https://…) também vão para a pasta Downloads (um PDF ou imagem pergunta antes "Ver ou Baixar"), assim como arquivos embutidos no app (<a href="manual.pdf" download>); no Android 9 e anteriores o app pede permissão de armazenamento primeiro. Arquivos grandes não são problema: o arquivo é enviado ao app em partes.

<a download> com um Blob - o padrão mais comum:

<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>

Uma URL data:, o FileSaver.js e navigator.msSaveBlob também funcionam:

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 de uma URL blob: ou data: significa "mostrar", não "salvar": um PDF abre no visualizador de PDF do app, outros tipos no app do telefone para aquele tipo:

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

Observações: Sem nome, o arquivo é salvo como download.<ext>; um segundo arquivo com o mesmo nome vira name (1).ext. Um objeto File traz o próprio nome (URL.createObjectURL(file)). <a href="/files/x" download="Name.pdf"> no seu próprio site mantém Name.pdf (o Chrome ignora o atributo em links de outros sites, e o app também). Se nenhum app do celular puder mostrar um arquivo window.open, ele é salvo em Downloads. Para deixar o usuário escolher a pasta, use showSaveFilePicker(). Para salvar sem um elemento <a>, use AppMint.saveBlob(blob, name) ou AppMint.downloadFile(base64, name, mime).

input-file padrão web#

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

Exemplo

Um campo de arquivo normal funciona no app como no Chrome: ele abre o seletor de fotos, a câmera ou o seletor de arquivos, e sua página recebe objetos File comuns.

Retorna: o evento change do campo com input.files (vazio quando o usuário cancela). Requer: fotos e arquivos não exigem nada. Para capture (a câmera), marque Câmera na Etapa 4 (Acesso) ao compilar; o app pede a permissão da câmera na primeira vez.

Fotos - abre o seletor de fotos do Android (sem pedido de permissão; a página recebe só o que o usuário escolheu):

<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>

Câmera - capture pula o seletor e abre a câmera direto:

<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>

Qualquer arquivo, ou vários tipos (extensões também funcionam):

<input type="file" accept=".pdf,.csv,application/json" multiple>
Você escreveO que abre
accept="image/*" / "video/*" / "image/*,video/*"o seletor de fotos (fotos, vídeos ou ambos)
acrescentar multipleo usuário pode escolher vários
acrescente capture (com Câmera marcada)a câmera diretamente - foto para image/*, vídeo para video/*
accept="image/*,.pdf" ou nenhum accepto seletor de arquivos do sistema

Observações: Sem Câmera marcada, capture é ignorado e o seletor abre. Quando Acesso nativo a pastas (SAF) está marcado, o app mostra o próprio menu "Selecionar origem do envio": Tirar foto / Gravar vídeo (se Câmera estiver marcada), Escolher arquivos e Enviar pasta (envia todos os arquivos de uma pasta). Com Armazenamento marcado no Android 12 e anteriores, escolher um arquivo que não seja foto pode pedir permissão de armazenamento primeiro. Celulares sem o seletor de fotos recebem o seletor de arquivos, que mostra as mesmas fotos.

showOpenFilePicker padrão web#

showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access

Exemplo

A API padrão File System Access funciona dentro do app, sobre o próprio seletor de documentos do Android: abra um arquivo e salve de volta NESSE ARQUIVO, crie um arquivo com uma folha Salvar como, ou obtenha uma pasta inteira para ler, criar e excluir arquivos.

Retorna: showOpenFilePicker(options) → Promise de um array de FileSystemFileHandle; showSaveFilePicker(options) → um FileSystemFileHandle; showDirectoryPicker(options) → um FileSystemDirectoryHandle. Um seletor cancelado rejeita AbortError; sem gesto do usuário rejeita SecurityError; um segundo seletor enquanto outro está aberto rejeita NotAllowedError. Requer: nada para ativar e nenhuma permissão - quem escolhe é o usuário.

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);
});

Escolha várias fotos de uma vez, começando na pasta Imagens:

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);
  }
});

Acrescente uma linha ao final de um arquivo de log sem reescrever o que já está lá:

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();
}

Escolha uma pasta de backup UMA VEZ e salve nela automaticamente a cada abertura seguinte - guarde o handle no IndexedDB, exatamente como no Chrome. O app mantém a permissão da pasta entre reinicializações, então queryPermission() responde 'granted' e o backup não precisa de toque nem de seletor:

// 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 a permissão foi removida (o usuário limpou o acesso do app ou excluiu a pasta), queryPermission() responde 'prompt'. A partir de um toque em botão, requestPermission() reabre o seletor de pastas NESSA pasta; escolher a mesma pasta responde 'granted', qualquer outra (ou cancelar) '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';
  }
});

Limpe uma pasta escolhida pelo usuário: exclua arquivos .tmp antigos e subpastas chamadas 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');
});

Observações: createWritable() segue o modelo de arquivo de troca da especificação - abort() (ou a página sumir) deixa o original intacto; { keepExistingData: true } parte do conteúdo atual; write({type:'seek'|'truncate'|'write', …}), seek() e truncate() funcionam. Um arquivo que o app de origem oferece somente leitura responde queryPermission({mode:'readwrite'}) com 'denied' e createWritable() rejeita NoModificationAllowedError. getFile() transmite o documento em fluxo, então arquivos grandes não são problema. Os handles podem ser guardados no IndexedDB (inclusive dentro de objetos e arrays) e usados de novo após o app reiniciar: o app mantém a permissão do Android para cada pasta e arquivo que um seletor retornou (os 64 mais recentes; o mais antigo é liberado primeiro). queryPermission() responde 'granted' enquanto essa permissão existir e 'prompt' quando ela sumir; requestPermission() então precisa de um toque e reabre o seletor na própria pasta do handle. Apps compilados antes de 2026-10-03 perdiam o handle ao reiniciar - compile de novo e o usuário escolhe a pasta mais uma vez. types viram o filtro de arquivos do seletor. Só a página de nível superior tem os seletores. Transportes internos: __fsPick (respondido com o evento appmint:fs), __fsFile, __fsList, __fsChild, __fsRemove, __fsPermission, __fsWritableOpen/Write/Truncate/Close/Abort. Apps criados por IA verificam can('fileHandles').

__dlAbort ponte#

window.WebToApk.__dlAbort(token: String)

Descarta o download [token] (a leitura da página falhou ou ela desistiu).

Exemplo

Documentado junto com WebToApk.__dlBegin - o exemplo de lá mostra este também.

Transporte interno por trás de <a download>, downloads de Blob, window.open(blobUrl) e AppMint.saveBlob / AppMint.downloadFile: o arquivo é enviado da página ao app em fatias, então uma exportação grande nunca vira uma string gigante na memória. As páginas usam o código de download padrão; nunca chame esses métodos diretamente, pois os métodos __ podem mudar sem aviso.

Retorna: __dlBegin(name, type, size) retorna um token (ou "" quando o celular não tem espaço); __dlChunk(token, base64) retorna false se uma fatia não pôde ser gravada; __dlEnd(token, action) conclui (save = Downloads, open = mostrar, share, copy). Requer: nada.

Use a API pública:

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"

Exporte um desenho de um canvas pelo mesmo caminho:

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');

Salve um backup em JSON que o usuário pode guardar, com uma mensagem ao terminar:

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.');
}

Observações: Implementado no shell do app (ShellDownloads.kt).

__dlBegin ponte#

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

Abre um download de [size] bytes chamado [name] ([type] = seu tipo MIME) que a página vai enviar em fluxo com __dlChunk. Retorna um token, ou "" quando não há espaço.

Exemplo

Transporte interno por trás de <a download>, downloads de Blob, window.open(blobUrl) e AppMint.saveBlob / AppMint.downloadFile: o arquivo é enviado da página ao app em fatias, então uma exportação grande nunca vira uma string gigante na memória. As páginas usam o código de download padrão; nunca chame esses métodos diretamente, pois os métodos __ podem mudar sem aviso.

Retorna: __dlBegin(name, type, size) retorna um token (ou "" quando o celular não tem espaço); __dlChunk(token, base64) retorna false se uma fatia não pôde ser gravada; __dlEnd(token, action) conclui (save = Downloads, open = mostrar, share, copy). Requer: nada.

Use a API pública:

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"

Exporte um desenho de um canvas pelo mesmo caminho:

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');

Salve um backup em JSON que o usuário pode guardar, com uma mensagem ao terminar:

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.');
}

Observações: Implementado no shell do app (ShellDownloads.kt).

__dlChunk ponte#

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

Uma fatia em base64 do download [token]; false quando a transferência falhou.

Exemplo

Documentado junto com WebToApk.__dlBegin - o exemplo de lá mostra este também.

Transporte interno por trás de <a download>, downloads de Blob, window.open(blobUrl) e AppMint.saveBlob / AppMint.downloadFile: o arquivo é enviado da página ao app em fatias, então uma exportação grande nunca vira uma string gigante na memória. As páginas usam o código de download padrão; nunca chame esses métodos diretamente, pois os métodos __ podem mudar sem aviso.

Retorna: __dlBegin(name, type, size) retorna um token (ou "" quando o celular não tem espaço); __dlChunk(token, base64) retorna false se uma fatia não pôde ser gravada; __dlEnd(token, action) conclui (save = Downloads, open = mostrar, share, copy). Requer: nada.

Use a API pública:

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"

Exporte um desenho de um canvas pelo mesmo caminho:

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');

Salve um backup em JSON que o usuário pode guardar, com uma mensagem ao terminar:

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.');
}

Observações: Implementado no shell do app (ShellDownloads.kt).

__dlEnd ponte#

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

Conclui o download [token] e age sobre ele: [action] "save" (Downloads, com a barra "Baixado · Abrir" e a notificação), "open" (visualizador de PDF ou o app do celular para o tipo), "share" (folha de compartilhamento) ou "copy" (imagem para a área de transferência).

Exemplo

Documentado junto com WebToApk.__dlBegin - o exemplo de lá mostra este também.

Transporte interno por trás de <a download>, downloads de Blob, window.open(blobUrl) e AppMint.saveBlob / AppMint.downloadFile: o arquivo é enviado da página ao app em fatias, então uma exportação grande nunca vira uma string gigante na memória. As páginas usam o código de download padrão; nunca chame esses métodos diretamente, pois os métodos __ podem mudar sem aviso.

Retorna: __dlBegin(name, type, size) retorna um token (ou "" quando o celular não tem espaço); __dlChunk(token, base64) retorna false se uma fatia não pôde ser gravada; __dlEnd(token, action) conclui (save = Downloads, open = mostrar, share, copy). Requer: nada.

Use a API pública:

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"

Exporte um desenho de um canvas pelo mesmo caminho:

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');

Salve um backup em JSON que o usuário pode guardar, com uma mensagem ao terminar:

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.');
}

Observações: Implementado no shell do app (ShellDownloads.kt).

__fsChild ponte#

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

getFileHandle / getDirectoryHandle (com `create`).

Exemplo

Transporte interno por trás de getFileHandle(name, {create}) e getDirectoryHandle(name, {create}). As páginas usam a API padrão; nunca chame isto diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok, kind, name, uri, canWrite} ou {ok:false, error} (NotFoundError, TypeMismatchError, NoModificationAllowedError). Requer: uma pasta de showDirectoryPicker().

Use a API pública:

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

Verifique se um arquivo já existe na pasta antes de criá-lo:

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;
  }
}

Salve as configurações do app como JSON em uma subpasta "config", criando ambas se necessário:

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)

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsFile ponte#

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

getFile(): `{ok, name, type, size, lastModified, url}` (o corpo é transmitido a partir da url).

Exemplo

Transporte interno por trás de FileSystemFileHandle.getFile(). As páginas usam a API padrão; nunca chame isso diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok, name, type, size, lastModified, url} - o conteúdo vem em fluxo de url (a origem do próprio app, com ranges) - ou {ok:false, error} (NotFoundError, TypeMismatchError, foreign_origin). Requer: um handle de um dos seletores.

Use a API pública:

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

Mostre uma imagem escolhida na página sem lê-la para dentro de uma string:

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 + ')';
});

Releia um arquivo que você abriu antes e perceba quando ele mudou ou foi excluído:

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;
  }
}

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsList ponte#

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

As entradas de um handle de diretório: `{ok, entries:[{kind, name, uri, canWrite}]}`.

Exemplo

Transporte interno por trás da iteração de um FileSystemDirectoryHandle (entries(), keys(), values(), for await). As páginas usam a API padrão; nunca chame isto diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok, entries:[{kind, name, uri, canWrite}]} ou {ok:false, error}. Requer: uma pasta de showDirectoryPicker().

Use a API pública:

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

Conte os arquivos de uma pasta e de todas as suas subpastas:

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');
});

Monte uma lista ordenada das fotos de uma pasta escolhida pelo usuário:

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
});

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsPermission ponte#

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

queryPermission() / requestPermission(): `{ok, state:'granted'|'prompt'}` para um handle cuja raiz escolhida é [rootUri] - 'granted' enquanto o app mantiver a permissão dessa escolha, entre reaberturas (a permissão é persistida; um handle volta do IndexedDB).

Exemplo

Transporte interno por trás de FileSystemHandle.queryPermission() e requestPermission(). As páginas usam a API padrão; nunca chame isto diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok:true, state:'granted'} enquanto o app mantiver a permissão da pasta ou do arquivo escolhido de onde vem o handle (mantida entre reinicializações), {ok:true, state:'prompt'} quando ela deixa de existir, ou {ok:false, error:'foreign_origin'}. Um arquivo somente leitura responde 'denied' para {mode:'readwrite'} na página, sem fazer chamada. Requer: um handle de um dos seletores, inclusive um lido de volta do IndexedDB.

Use a API pública:

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();
}

Peça de novo a partir de um toque quando a permissão tiver sumido - o seletor de pastas abre nessa pasta, e só a mesma pasta concede:

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';
});

Observações: Implementado no shell do app (FileSystemAccess.kt). Veja showOpenFilePicker para a API File System Access completa.

__fsPick ponte#

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

Abre o seletor de [kind] (open | save | directory); os handles chegam como `appmint:fs`.

Exemplo

Transporte interno por trás de showOpenFilePicker(), showSaveFilePicker() e showDirectoryPicker(). As páginas usam a API padrão; nunca chame isto diretamente - os métodos __ podem mudar sem aviso.

Retorna: {ok:true} quando o seletor está na tela, ou {ok:false, error} (NotAllowedError quando já há um aberto, foreign_origin). Os handles chegam como o evento appmint:fs {requestId, handles:[{kind, name, uri, canWrite}]} ou {requestId, error:'cancelled'}. Requer: nada para ativar.

Use a API pública:

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

Salve um relatório com um nome sugerido, a partir de um toque em botão (os seletores precisam de um gesto do usuário):

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
  }
});

Deixe o usuário escolher várias fotos de uma vez, começando na pasta Imagens:

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());
});

Observações: Implementado no shell do app (FileSystemAccess.kt). O mesmo código de página funciona sem alterações no Chrome para computador.

__fsRemove ponte#

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

removeEntry(name, {recursive}).

Exemplo

Transporte interno por trás de FileSystemDirectoryHandle.removeEntry(name, {recursive}). As páginas usam a API padrão; nunca chame isso diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok:true} ou {ok:false, error} (NotFoundError, InvalidModificationError para uma pasta não vazia sem recursive). Requer: uma pasta de showDirectoryPicker().

Use a API pública:

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

Exclua um arquivo e trate "já não existe" como concluído:

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

Remova uma pasta, perguntando antes apenas quando ainda houver algo dentro:

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;
}

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsWritableAbort ponte#

window.WebToApk.__fsWritableAbort(token: String)

abort(): a troca é descartada; o documento fica intacto.

Exemplo

Transporte interno por trás de FileSystemWritableFileStream.abort() - a cópia de troca é descartada e o arquivo continua como estava. As páginas usam a API padrão; nunca chame isto diretamente - os métodos __ podem mudar sem aviso.

Retorna: nada. Requer: um fluxo gravável aberto.

Use a API pública:

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

Deixe o usuário cancelar uma exportação longa; nada gravado pela metade chega ao arquivo:

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';
}

Baixe direto para um arquivo com pipeTo; interromper o download cancela a gravação:

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;
  }
}

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsWritableClose ponte#

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

close(): a troca substitui o documento.

Exemplo

Transporte interno por trás de FileSystemWritableFileStream.close() - a cópia de troca substitui o arquivo. As páginas usam a API padrão; nunca chame isto diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok:true} ou {ok:false, error} (NoModificationAllowedError, InvalidModificationError quando o app do arquivo não consegue encurtá-lo). Requer: um stream gravável aberto.

Use a API pública:

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

Diga ao usuário por que um salvamento não foi concluído (close() é onde ele pode falhar):

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;
  }
}

Grave um arquivo em várias partes; os leitores continuam vendo o conteúdo antigo até que o 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');
}

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsWritableOpen ponte#

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

createWritable(): um arquivo de troca, copiado do arquivo quando [keepExisting].

Exemplo

Transporte interno por trás de FileSystemFileHandle.createWritable({keepExistingData}). As páginas usam a API padrão; nunca chame isso diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok, token} para uma cópia de troca privada, ou {ok:false, error} (NoModificationAllowedError para um arquivo somente leitura). Requer: um handle de um dos seletores.

Use a API pública:

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();

Acrescente uma linha ao final de um arquivo de log existente, mantendo o que já está nele:

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
}

Salve um canvas como PNG e deixe o arquivo intocado se algo der errado:

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);
  }
});

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsWritableTruncate ponte#

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

truncate(size) em um fluxo gravável.

Exemplo

Transporte interno por trás de FileSystemWritableFileStream.truncate(size). As páginas usam a API padrão; nunca chame isso diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok:true} ou {ok:false, error}. Requer: um stream gravável aberto.

Use a API pública:

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

Mantenha só os primeiros 1.000 bytes de um arquivo e corte tudo o que vem depois:

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();
}

Reserve espaço: truncar para um tamanho maior aumenta o arquivo, e os novos bytes são lidos de volta como zeros:

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');
}

Observações: Implementado no shell do app (FileSystemAccess.kt).

__fsWritableWrite ponte#

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

Um bloco do write() de um fluxo gravável, em [position].

Exemplo

Transporte interno por trás de FileSystemWritableFileStream.write() - um bloco em uma posição da cópia de troca. As páginas usam a API padrão; nunca chame isto diretamente - os métodos __ podem mudar sem aviso.

Retorna: JSON {ok:true} ou {ok:false, error}. Requer: um stream gravável aberto.

Use a API pública:

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

Acrescente uma linha ao final de um arquivo de log escolhido pelo usuário (um seletor de arquivos precisa de um toque):

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();
});

Salve um desenho de canvas como um novo PNG com "Salvar como" (um Blob é gravado como seus bytes brutos):

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();
});

Observações: Implementado no shell do app (FileSystemAccess.kt). Dados grandes são enviados em blocos.

getMaxIoChunkSize ponte#

window.WebToApk.getMaxIoChunkSize(): Long

O limite de bytes por chamada de [readFileBase64] / [writeFileBase64], para cálculos de blocos.

Exemplo

O limite de bytes por chamada de readFileBase64 / writeFileBase64 (4 MB). Normalmente as páginas usam WebToApkFS.maxChunkSize().

Retorna: (síncrono) um número (4194304). Requer: nada para chamá-lo; os métodos de leitura/gravação que ele descreve exigem Acesso nativo a pastas (SAF) marcado na Etapa 4 (Acesso).

O jeito público:

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

A chamada bruta:

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');

Calcule quantas chamadas de ponte um arquivo vai exigir, para dimensionar uma barra de progresso antes de copiá-lo:

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;
}

Copie um arquivo para outro, um bloco de tamanho máximo por vez:

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)

Observações: Uma gravação maior que isso é recusada com chunk too large (max N bytes); uma leitura maior é cortada nesse tamanho.

pickFile ponte#

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

Abre o seletor de arquivo único do sistema (ACTION_OPEN_DOCUMENT). Diferente de [requestFolderAccess], isto concede acesso a exatamente um arquivo, que é o que um editor ou um destruidor de arquivos deve pedir; a permissão de leitura/gravação é persistida para que a URI continue funcionando depois que o processo for encerrado. A resposta chega em `WebToApkOnFilePickResult(success, fileJson, requestId)`; o `error` de uma falha é "cancelled", "disabled", "no-picker", "foreign_origin" ou "busy" (já há um seletor aberto para uma solicitação anterior, que mantém a própria resposta). @param mimeFilter por ex. "image/\*", "application/pdf", ou "" para qualquer arquivo. (O asterisco leva barra invertida só porque comentários de bloco do Kotlin se aninham.)

Exemplo

Abre o seletor do sistema para UM arquivo. As páginas normalmente usam o helper com Promise WebToApkFS.pickFile(mimeFilter); este método bruto responde por um callback.

Retorna: nada por enquanto. Depois o shell chama window.WebToApkOnFilePickResult(success, fileJson, requestId), onde fileJson é uma string JSON {ok, uri, name, size, mime} em caso de sucesso ou {ok:false, uri:'', error} com error = 'cancelled', 'disabled', 'no-picker', 'busy' ou 'foreign_origin'. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

O jeito público (recomendado):

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

A chamada bruta, comparando por conta própria 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
  });
}

Anexe um PDF e mostre seu nome e tamanho, distinguindo um cancelamento de um problema de verdade:

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
});

Abra um arquivo de texto e carregue seu conteúdo em um 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
}

Observações: Só uma seleção pode ficar aberta por vez: uma chamada feita enquanto uma está aberta é respondida na hora com error:'busy', e a aberta mantém sua própria resposta. A concessão é mantida (leitura e gravação) até você chamar releaseFileAccess(uri). O arquivo não é lido aqui — use readFileBase64 / WebToApkFS.readBytes com o uri.

readFileBase64 ponte#

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

Lê até [length] bytes brutos a partir de [offset] e os retorna em base64. @param pathOrUri uma URI `content://` de [pickFile], ou um caminho relativo à pasta concedida por [requestFolderAccess]. @param length bytes a ler; limitado a 4 MB por chamada. 0 ou negativo significa "o máximo que uma chamada permite". @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` diz a um laço de leitura em blocos quando parar, sem precisar comparar com `size`.

Exemplo

Lê bytes brutos de um arquivo a partir de um deslocamento em bytes e os retorna em base64. Normalmente as páginas usam WebToApkFS.readBytes(pathOrUri, offset, length) (mesmo resultado, já interpretado) ou WebToApkFS.readAll(pathOrUri).

Retorna: (síncrono) uma string JSON {ok:true, base64, bytesRead, offset, size, eof} ou {ok:false, error}. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

O jeito público:

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

A chamada bruta - note que o resultado é uma string que você deve passar por JSON.parse, não um base64 puro:

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);
  }
}

Confira se um arquivo escolhido é mesmo um PNG lendo apenas seus primeiros 8 bytes:

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; });
}

Leia um arquivo grande bloco por bloco com uma barra de progresso, sem nunca mantê-lo inteiro na memória:

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
}

Observações: pathOrUri é uma URI content:// vinda de pickFile ou um caminho relativo à pasta de requestFolderAccess. No máximo 4 MB por chamada (getMaxIoChunkSize()); repita com bytesRead até eof. Erros: not-found, disabled, cannot-open, offset must be >= 0, offset past end of file (N bytes), foreign_origin (uma página de um site que não é o do próprio app).

releaseFileAccess ponte#

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

Libera uma permissão persistida de arquivo único obtida por [pickFile]. O Android limita quantas permissões de URI um app pode persistir, então um app que escolhe muitos arquivos deve liberar os que já terminou de usar.

Exemplo

Libera a permissão de leitura/gravação mantida para um arquivo escolhido com pickFile. Normalmente as páginas usam WebToApkFS.releaseFile(uri), que retorna um booleano de verdade.

Retorna: (síncrono) a string "true" ou "false". Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar (caso contrário, sempre "false").

O jeito público:

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

A chamada bruta:

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');
}

Mantenha uma lista de "arquivos recentes" com no máximo 10 e libere a permissão de qualquer arquivo que sair dela:

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));
}

Feche o documento atual: libere sua permissão e esqueça a URI salva:

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 = '';
});

Observações: O Android limita quantas concessões de arquivo um app pode manter, então libere os arquivos de que não precisa mais. O arquivo em si não é alterado.

statFile ponte#

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

Metadados de arquivo ou pasta sem ler nada deles. Um caminho relativo nomeia um arquivo ou uma subpasta da pasta autorizada; uma pasta responde `isDirectory: true` com `size` 0 e `mime` `vnd.android.document/directory`. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, ou `{ok:false, error}` com `not-found`, `disabled` ou `foreign_origin`.

Exemplo

Lê os metadados de um arquivo (nome, tamanho, tipo) sem ler seus bytes. Normalmente as páginas usam WebToApkFS.stat(pathOrUri), que interpreta o JSON para você.

Retorna: (síncrono) uma string JSON {ok, name, size, mime, lastModified, uri, isDirectory, canWrite}, ou {ok:false, error} ('not-found', 'disabled', 'foreign_origin'). Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

O jeito público:

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

A chamada bruta:

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);
}

Recuse um arquivo grande demais antes de ler qualquer parte dele:

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;
}

Procure uma subpasta da pasta autorizada e só ofereça "Salvar aqui" quando ela for gravável:

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;
}

Observações: Aceita uma URI content:// vinda de pickFile ou um caminho relativo à pasta autorizada com requestFolderAccess. Só responde às páginas do próprio app ou ao site configurado; uma página de qualquer outro site recebe foreign_origin.

writeFileBase64 ponte#

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

Grava bytes brutos e confirma quantos chegaram. @param mode `"patch"` grava em [offset] deixando o resto do arquivo intacto (o que uma sobrescrita em várias passadas precisa), `"truncate"` substitui o arquivo, `"append"` ignora [offset] e grava no fim. @return JSON `{ok, bytesWritten, offset, size, error}` — `size` é relido do arquivo após um fsync, para que quem chama possa verificar a gravação de verdade em vez de confiar em um booleano.

Exemplo

Grava bytes em base64 em um arquivo a partir de um deslocamento (patch), sobre o arquivo inteiro (truncate) ou no final (append). Normalmente as páginas usam WebToApkFS.writeBytes(pathOrUri, base64, offset, mode) ou WebToApkFS.writeAll(pathOrUri, base64).

Retorna: (síncrono) uma string JSON {ok, bytesWritten, offset, size} (size relido após um fsync) ou {ok:false, error}. Requer: marque Acesso nativo a pastas (SAF) na Etapa 4 (Acesso) ao compilar.

O jeito público:

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

A chamada bruta:

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');
}

Salve um desenho de canvas como PNG em um arquivo escolhido pelo usuário, substituindo o que estava lá:

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);
});

Sobrescreva um arquivo no lugar com zeros antes de excluí-lo e confirme que cada passada foi gravada:

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
}

Observações: O modo é 'patch' (padrão quando vazio), 'truncate' ou 'append'. Uma chamada aceita no máximo 4 MB de bytes decodificados (getMaxIoChunkSize()). Um caminho relativo é criado dentro da pasta concedida se não existir. Erros: disabled, foreign_origin (a página na tela não é do próprio app), bad mode: x, offset must be >= 0, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

Gerado a partir do runtime do app e de seus arquivos de exemplo a cada build da documentação. Leia em Markdown · Todas as famílias.

Conferido com a ponte atual em 22/10/2026.