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.saveBlobUm 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.csvUm 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" messageObservaçõ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ê escreve | O que abre |
|---|---|
accept="image/*" / "video/*" / "image/*,video/*" | o seletor de fotos (fotos, vídeos ou ambos) |
acrescentar multiple | o 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 accept | o 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 thisDiga 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.