Buat APK Gratis

API jembatan JavaScript

File: satu file sekali waktu

Pemilih sistem dan baca/tulis biner per potongan, untuk file yang terlalu besar untuk disimpan dalam string.

AppMint.downloadFile helper#

AppMint.downloadFile(data, fileName, mimeType?)

Contoh

Menyimpan file yang dibuat halaman Anda - dari string base64, URL data:, URL blob: atau Blob - dengan nama file yang Anda pilih. Lembar "Simpan sebagai…" Android terbuka dengan nama itu sudah terisi.

Mengembalikan: (sinkron) true setelah penyimpanan diserahkan ke Android (false hanya untuk URL data: yang tidak bisa diurai). Melempar TypeError jika data bukan Blob atau string. Pengguna tetap memilih folder, dan bisa membatalkan. Membutuhkan: tidak ada - selalu tersedia di aplikasi.

String Base64 ditambah tipe 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();
  }
}

URL data: (tipe MIME berasal dari URL) atau 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

PDF base64 yang dikembalikan server Anda dalam JSON; nama tanpa ekstensi akan mendapat .pdf dari tipe 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
}

URL blob: yang sudah Anda buat untuk pratinjau di layar:

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

Catatan: Hanya bagian terakhir dari fileName yang dipakai (tanpa folder). Nama tanpa ekstensi akan mendapat satu dari tipe MIME. Argumen ketiga hanya dipakai untuk string base64 biasa. Jika ponsel sama sekali tidak punya layar "Simpan sebagai…", file ditulis langsung ke Downloads. Kode <a download> biasa Anda juga berfungsi - lihat downloads.

AppMint.saveBlob helper#

AppMint.saveBlob(blob, fileName)

Contoh

Menyimpan Blob (atau File) dengan nama yang Anda pilih. Lembar "Simpan sebagai…" Android terbuka dengan nama itu sudah terisi.

Mengembalikan: (sinkron) true setelah penyimpanan diserahkan ke Android. Melempar TypeError jika argumen pertama bukan Blob. Membutuhkan: tidak ada - selalu tersedia di aplikasi.

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

Sebuah File membawa namanya sendiri, jadi nama bisa dihilangkan:

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

File dari server Anda yang butuh header masuk, disimpan dengan nama pilihan Anda:

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

Beberapa file dikemas menjadi satu ZIP (di sini dengan JSZip) dan disimpan sekaligus:

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

Catatan: Blob dibaca ke memori dan dikirim ke Android sekaligus, jadi blob yang sangat besar (ratusan MB) bisa kehabisan memori. Pengguna memilih folder dan dapat membatalkan; tidak ada yang memberi tahu halaman mana yang terjadi.

AppMint.setDownloadName helper#

AppMint.setDownloadName(url, fileName)

Contoh

Memberi URL blob: atau data: nama file untuk penyimpanannya, sebelum kode Anda (atau sebuah library) memicu unduhan.

Mengembalikan: tidak ada. Membutuhkan: tidak ada - selalu tersedia di aplikasi.

Berguna saat sebuah library mengunduh URL yang Anda buat sebelumnya dan Anda tidak bisa memberinya nama:

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"

PDF yang dibuka halaman Anda dengan window.open masuk ke penampil PDF ponsel; beri nama lebih dulu agar tidak menjadi "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');
}

URL data: bekerja dengan cara yang sama, misalnya gambar grafik yang ditautkan library tanpa atribut 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';

Catatan: Nama tetap diingat bahkan setelah URL.revokeObjectURL(url), karena sebagian library mencabutnya sebelum penyimpanan selesai. Anda jarang membutuhkan ini: <a download="name">, URL.createObjectURL(file) (sebuah File punya nama) dan saveAs(blob, name) dari FileSaver semuanya otomatis terdeteksi. Nama 'download' diabaikan sebagai "tanpa nama".

WebToApkFS.maxChunkSize helper#

WebToApkFS.maxChunkSize()

Contoh

Memberi tahu jumlah byte terbanyak yang bisa dipindahkan oleh satu panggilan readBytes atau writeBytes (4 MB), sehingga Anda bisa mengatur ukuran potongan pada perulangan.

Mengembalikan: (sinkron) jumlah byte, atau 0 bila metode bridge tidak ada. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build (itulah yang menyuntikkan 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 };
  }
}

Tampilkan progres baca: ukuran chunk memberi tahu berapa langkah yang dibutuhkan sebuah file:

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

Tambahkan array byte besar ke file di folder yang diizinkan, satu potongan yang diperbolehkan setiap kali:

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

Catatan: Potongan yang lebih kecil menjaga halaman tetap responsif (setiap panggilan bersifat sinkron); batas ini hanya menyatakan seberapa besar satu panggilan boleh.

WebToApkFS.pickFile helper#

WebToApkFS.pickFile(mimeFilter)

Contoh

Membuka pemilih file Android agar pengguna memilih SATU file; halaman Anda kemudian bisa membaca dan menulis byte asli file itu, bahkan setelah aplikasi dimulai ulang.

Mengembalikan: Promise<{ok, uri, name, size, mime, error}>. Jika berhasil ok bernilai true dan uri adalah alamat content:// yang Anda berikan ke stat, readBytes, writeBytes. Jika gagal ok bernilai false dan error adalah 'cancelled', 'disabled', 'no-picker', 'busy' (sebuah pemilih sudah terbuka untuk panggilan sebelumnya, yang tetap memegang jawabannya sendiri), 'foreign_origin' atau 'unsupported' (tanpa aplikasi). Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

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
}

Pakai lagi uri yang tersimpan nanti, tanpa bertanya kepada pengguna untuk kedua kalinya:

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

Filter hanya menerima SATU pola, jadi pilih secara luas dan periksa sendiri namanya:

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

Catatan: Filternya satu pola tipe MIME ('image/*', 'text/csv'), bukan daftar. Pemilih meminta akses baca DAN tulis dan mempertahankannya. Android membatasi jumlah file yang boleh disimpan aplikasi dengan cara ini, jadi panggil WebToApkFS.releaseFile(uri) saat Anda selesai dengan sebuah file. Untuk seluruh folder, gunakan WebToApkFS.requestAccess(). WebToApkFS dipasang saat Native Folder Access atau Secure Keys dicentang; panggilannya hanya berfungsi dengan Native Folder Access (WebToApkFS.isAvailable()).

WebToApkFS.readAll helper#

WebToApkFS.readAll(pathOrUri)

Contoh

Membaca seluruh file menjadi satu string base64. Ini melakukan perulangan readBytes untuk Anda, 4 MB sekali baca.

Mengembalikan: (sinkron) {ok:true, base64, size}, atau {ok:false, error} dari potongan pertama yang gagal. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Muat gambar terpilih ke dalam sebuah <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;
}

Baca file teks UTF-8 (berfungsi untuk bahasa apa pun, tidak hanya bahasa Inggris):

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

Tampilkan checksum SHA-256 sebuah file, misalnya untuk membandingkan unduhan dengan yang dipublikasikan:

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

Catatan: Seluruh file berakhir di memori (sebagai byte lalu sebagai base64), dan panggilan ini memblokir halaman selama berjalan. Pakai untuk file yang mudah muat di memori; untuk file besar, ulangi WebToApkFS.readBytes dan tangani tiap potongan. Untuk teks biasa di dalam folder yang diizinkan, WebToApkFS.readText(path) lebih sederhana.

WebToApkFS.readBytes helper#

WebToApkFS.readBytes(pathOrUri, offset, length)

Contoh

Membaca byte asli sebuah file, mulai dari posisi byte tertentu, sebagai base64. Aman untuk gambar, PDF, ZIP - tidak ada yang berubah oleh dekode teks.

Mengembalikan: (sinkron) {ok:true, base64, bytesRead, offset, size, eof} atau {ok:false, error} ('not-found', 'disabled', 'cannot-open', 'offset past end of file (N bytes)', 'foreign_origin', 'unsupported'). Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Baca 64 KB pertama, misalnya untuk memeriksa header file:

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

Baca file besar sepotong demi sepotong, sehingga tidak perlu muat di memori sekaligus:

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

Baca hanya bagian akhir sebuah file, misalnya tag 128 byte di akhir 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
}

Catatan: Satu panggilan membaca paling banyak WebToApkFS.maxChunkSize() byte (4 MB); length 0 berarti "sebanyak yang diizinkan satu panggilan". Ulangi pada bytesRead dan berhenti pada eof. Panggilan ini sinkron, jadi jaga potongan tetap sedang di thread UI. Untuk file kecil, WebToApkFS.readAll(uri) melakukan perulangannya untuk Anda.

WebToApkFS.readSealed helper#

WebToApkFS.readSealed(pathOrUri, alias)

Contoh

Membaca file yang ditulis oleh WebToApkFS.writeSealed dan mendekripsinya dengan kunci keystore yang sama.

Mengembalikan: (sinkron) {ok:true, base64} (byte asli), atau {ok:false, error} - 'keystore-disabled', 'corrupt-sealed-file', kode keystore ('no-such-key' setelah kunci dihapus, 'auth-required', 'key-invalidated') atau kesalahan file ('not-found' …). Membutuhkan: centang Native Folder Access (SAF) dan Secure Keys (enkripsi) di Langkah 4 (Akses) saat Anda membuat build.

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

Simpan pengaturan aplikasi dalam file JSON tersegel, dengan nilai default sebelum penyimpanan pertama:

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

Pindahkan file tersegel ke kunci baru, lalu hancurkan kunci lama:

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

Catatan: Gagal selamanya setelah AppMintKeystore.deleteKey(alias) dijalankan - itulah tujuannya. Seluruh file dibaca dan didekripsi di memori, secara sinkron; jaga file tersegel tetap kecil (catatan, kunci, token).

WebToApkFS.releaseFile helper#

WebToApkFS.releaseFile(uri)

Contoh

Mengembalikan akses jangka panjang yang didapat aplikasi Anda saat pengguna memilih file dengan WebToApkFS.pickFile.

Mengembalikan: (sinkron) true jika izin dilepas, false jika tidak (tanpa izin, URI buruk, tidak di dalam aplikasi). Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

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
}

Saat pengguna memilih file default baru, kembalikan izin untuk file yang lama:

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

Tombol "Lupakan file terbaru" yang melepaskan setiap file yang diingat aplikasi:

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

Catatan: Android membatasi jumlah file terpilih yang boleh dipertahankan aksesnya oleh satu aplikasi. Aplikasi yang memilih banyak file (pengimpor, penampil) sebaiknya melepas tiap file setelah selesai. Setelah dilepas, stat / readBytes / writeBytes pada URI itu menjawab not-found. Ini tidak menghapus file.

WebToApkFS.stat helper#

WebToApkFS.stat(pathOrUri)

Contoh

Memberi tahu nama, ukuran, tipe, dan tanggal file tanpa membaca isinya.

Mengembalikan: (sinkron) {ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}, atau {ok:false, error} dengan error = 'not-found', 'disabled', 'foreign_origin' atau 'unsupported'. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

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

Path di dalam folder yang diizinkan pengguna dengan WebToApkFS.requestAccess() juga berfungsi:

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

Impor ulang file yang diingat hanya jika berubah sejak impor terakhir:

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

Catatan: pathOrUri adalah URI content:// dari pickFile() atau path relatif terhadap folder yang diizinkan. URI yang tidak pernah diberikan ke aplikasi Anda mengembalikan not-found. lastModified adalah milidetik sejak 1970 (0 jika penyedia tidak menyebutkannya).

WebToApkFS.writeAll helper#

WebToApkFS.writeAll(pathOrUri, base64)

Contoh

Mengganti seluruh isi file dengan byte Anda. Data dipecah menjadi potongan 4 MB untuk Anda: potongan pertama memotong file, sisanya ditambal setelahnya.

Mengembalikan: (sinkron) {ok:true, bytesWritten}, atau {ok:false, error} dari bagian pertama yang gagal. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Simpan teks UTF-8 (bahasa apa pun) kembali ke file yang dipilih pengguna:

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

Tulis gambar canvas ke folder yang diizinkan (file dibuat jika belum ada):

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

Simpan cadangan JSON ke folder yang diizinkan, meminta folder dulu bila belum ada, lalu pastikan ukurannya di disk:

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

Catatan: Seluruh input berada di memori selama ditulis, dan panggilan ini memblokir halaman. Jika bagian berikutnya gagal, file sudah berisi bagian-bagian sebelumnya - periksa ok dan beri tahu pengguna. Untuk merahasiakan file, gunakan WebToApkFS.writeSealed sebagai gantinya.

WebToApkFS.writeBytes helper#

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

Contoh

Menulis byte sungguhan ke file dan memberi tahu berapa banyak yang berhasil. Bisa menambal byte di tengah, mengganti seluruh file, atau menambahkan di akhir.

Mengembalikan: (sinkron) {ok, bytesWritten, offset, size} - size dibaca kembali dari file setelah data di-flush ke disk - atau {ok:false, error}. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Patch - ubah 4 byte pada posisi 100, biarkan sisa file:

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

Truncate - ganti seluruh isi file (offset diabaikan):

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

Append - tambahkan ke akhir (offset diabaikan):

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

Catatan: mode adalah 'patch' (default), 'truncate' atau 'append'. Paling banyak 4 MB per panggilan (WebToApkFS.maxChunkSize()); untuk data lebih besar gunakan WebToApkFS.writeAll. Path di dalam folder yang diizinkan dibuat jika belum ada; URI content:// terpilih harus yang dipilih pengguna. btoa hanya menerima teks Latin-1 - untuk teks lain, enkode ke byte dulu (lihat WebToApkFS.writeAll). Kesalahan: disabled, foreign_origin (halaman di layar bukan milik aplikasi sendiri), 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)

Contoh

Menulis file terenkripsi dengan kunci berbasis perangkat keras dari AppMintKeystore. Menghapus kunci itu kemudian membuat file tidak bisa dibaca siapa pun - satu-satunya "penghapusan aman" yang sesungguhnya di ponsel.

Mengembalikan: (sinkron) {ok:true, bytesWritten} (ukuran terenkripsi di disk), atau {ok:false, error} - 'keystore-disabled', kode keystore seperti 'no-such-key', 'auth-required', 'key-invalidated', atau kesalahan file. Membutuhkan: centang Native Folder Access (SAF) dan Secure Keys (encryption) di Langkah 4 (Akses) saat Anda membuat build.

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

Hapus sungguh-sungguh - hancurkan kuncinya, lalu hapus filenya:

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

Segel foto yang dipilih pengguna, sebagai byte mentah, ke dalam folder yang telah mereka izinkan:

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

Kunci buku harian yang hanya berfungsi selama 5 menit setelah ponsel dibuka kuncinya, dengan setiap kesalahan dijelaskan:

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

Catatan: Jangan "timpa dengan nol" untuk menghapus: penyimpanan flash ponsel menyimpan blok lama. Format file berupa daftar potongan AES-GCM berawalan panjang; bacalah kembali hanya dengan WebToApkFS.readSealed dan alias yang sama. Kunci yang dibuat dengan {requireAuth:true} menjawab auth-required sampai pengguna baru saja membuka kunci ponsel.

downloads standar web#

<a download> and Blob downloads

Contoh

Kode unduhan web normal Anda berfungsi di dalam aplikasi, seperti di Chrome. WebView mengabaikan unduhan blob: dan data: dengan sendirinya; aplikasi menangkapnya dan menyimpan file ke folder Unduhan ponsel dengan nama file Anda, lalu menampilkan "Terunduh <nama> · Buka" dan notifikasi "Unduhan selesai".

Mengembalikan: tidak ada untuk halaman. Membutuhkan: tidak ada yang perlu diaktifkan. Unduhan dari alamat web (https://…) juga masuk ke folder Downloads (PDF atau gambar lebih dulu menanyakan "Lihat atau Unduh"), begitu pula file yang dikemas di dalam aplikasi (<a href="manual.pdf" download>); di Android 9 dan yang lebih lama, aplikasi meminta izin penyimpanan lebih dulu. File besar tidak masalah: file dialirkan ke aplikasi per potongan.

<a download> dengan Blob - pola yang paling umum:

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

URL data:, FileSaver.js, dan navigator.msSaveBlob juga berfungsi:

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 dari URL blob: atau data: berarti "tampilkan", bukan "simpan": PDF dibuka di penampil PDF aplikasi, jenis lain di aplikasi ponsel untuk jenis itu:

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

Catatan: Tanpa nama, file disimpan sebagai download.<ext>; file kedua dengan nama sama menjadi name (1).ext. Objek File membawa namanya sendiri (URL.createObjectURL(file)). <a href="/files/x" download="Name.pdf"> di situs Anda sendiri mempertahankan Name.pdf (Chrome mengabaikan atribut itu pada tautan situs lain, begitu pula aplikasi). Jika tidak ada aplikasi di ponsel yang bisa menampilkan file window.open, file disimpan ke Downloads. Agar pengguna bisa memilih folder, gunakan showSaveFilePicker(). Untuk menyimpan tanpa elemen <a>, gunakan AppMint.saveBlob(blob, name) atau AppMint.downloadFile(base64, name, mime).

input-file standar web#

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

Contoh

Input file biasa berfungsi di aplikasi seperti di Chrome: membuka pemilih foto, kamera, atau pemilih file, dan halaman Anda mendapat objek File biasa.

Mengembalikan: event change milik input dengan input.files (kosong jika pengguna membatalkan). Membutuhkan: foto dan file tidak membutuhkan apa pun. Untuk capture (kamera), centang Camera di Langkah 4 (Akses) saat Anda membuat build; aplikasi meminta izin kamera saat pertama kali.

Foto - membuka pemilih foto Android (tanpa permintaan izin; halaman hanya mendapat yang dipilih pengguna):

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

Kamera - capture melewati pemilih dan langsung membuka kamera:

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

File apa saja, atau beberapa jenis (ekstensi juga berfungsi):

<input type="file" accept=".pdf,.csv,application/json" multiple>
Anda menulisYang terbuka
accept="image/*" / "video/*" / "image/*,video/*"pemilih foto (foto, video, atau keduanya)
tambahkan multiplepengguna bisa memilih beberapa
tambahkan capture (dengan Camera dicentang)langsung ke kamera - foto untuk image/*, video untuk video/*
accept="image/*,.pdf" atau tidak acceptpemilih file sistem

Catatan: Tanpa Camera dicentang, capture diabaikan dan pemilih terbuka. Saat Native Folder Access (SAF) dicentang, aplikasi menampilkan menu "Pilih Sumber Unggahan" sendiri: Ambil Foto / Rekam Video (jika Camera dicentang), Pilih File, dan Unggah Folder (mengirim setiap file dalam folder). Dengan Storage dicentang di Android 12 ke bawah, memilih file non-foto mungkin meminta izin penyimpanan lebih dulu. Ponsel tanpa pemilih foto mendapat pemilih file, yang menampilkan foto yang sama.

showOpenFilePicker standar web#

showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access

Contoh

File System Access API standar berfungsi di dalam aplikasi, di atas pemilih dokumen bawaan Android: buka file dan simpan kembali KE FILE ITU, buat file dengan lembar Simpan sebagai, atau dapatkan seluruh folder untuk membaca, membuat, dan menghapus file di dalamnya.

Mengembalikan: showOpenFilePicker(options) → Promise berisi array FileSystemFileHandle; showSaveFilePicker(options) → satu FileSystemFileHandle; showDirectoryPicker(options) → satu FileSystemDirectoryHandle. Pemilih yang dibatalkan menolak dengan AbortError; tanpa gestur pengguna menolak dengan SecurityError; pemilih kedua saat satu pemilih masih terbuka menolak dengan NotAllowedError. Membutuhkan: tidak ada yang perlu diaktifkan dan tanpa izin - pengguna yang memilih.

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

Pilih beberapa foto sekaligus, dimulai dari folder Pictures:

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

Tambahkan satu baris ke akhir file log tanpa menulis ulang yang sudah ada:

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

Pilih folder cadangan SEKALI saja dan simpan ke dalamnya secara otomatis di setiap peluncuran berikutnya - simpan handle di IndexedDB, persis seperti di Chrome. Aplikasi mempertahankan izin folder itu setelah dimulai ulang, sehingga queryPermission() menjawab 'granted' dan pencadangan tidak perlu ketukan maupun pemilih file:

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

Jika izin dicabut (pengguna menghapus akses aplikasi, atau menghapus folder), queryPermission() menjawab 'prompt'. Dari ketukan tombol, requestPermission() membuka kembali pemilih folder DI folder tersebut; memilih folder yang sama menjawab 'granted', selain itu (atau dibatalkan) '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';
  }
});

Bersihkan folder yang dipilih pengguna: hapus file .tmp lama dan sub-folder bernama 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');
});

Catatan: createWritable() mengikuti model swap-file dari spesifikasi - abort() (atau halaman yang ditutup) membiarkan file asli tidak tersentuh; { keepExistingData: true } dimulai dari isi saat ini; write({type:'seek'|'truncate'|'write', …}), seek() dan truncate() berfungsi. File yang oleh aplikasinya hanya ditawarkan sebagai hanya-baca menjawab queryPermission({mode:'readwrite'}) dengan 'denied' dan createWritable() menolak dengan NoModificationAllowedError. getFile() mengalirkan dokumen, jadi file besar tidak masalah. Handle bisa disimpan di IndexedDB (termasuk di dalam objek dan array) dan dipakai lagi setelah aplikasi dimulai ulang: aplikasi mempertahankan izin Android untuk setiap folder dan file yang dikembalikan pemilih (64 terbaru; yang tertua dilepas lebih dulu). queryPermission() menjawab 'granted' selama izin itu dipegang dan 'prompt' setelah hilang; requestPermission() kemudian membutuhkan ketukan dan membuka kembali pemilih di folder milik handle itu sendiri. Aplikasi yang dibuat sebelum 2026-10-03 kehilangan handle saat dimulai ulang - buat ulang, dan pengguna memilih folder sekali lagi. types menjadi filter file pada pemilih. Hanya halaman tingkat atas yang memiliki pemilih. Jalur internal: __fsPick (dijawab dengan event appmint:fs), __fsFile, __fsList, __fsChild, __fsRemove, __fsPermission, __fsWritableOpen/Write/Truncate/Close/Abort. Aplikasi buatan AI memeriksa can('fileHandles').

__dlAbort jembatan#

window.WebToApk.__dlAbort(token: String)

Membuang unduhan [token] (pembacaan halaman gagal atau halaman menyerah).

Contoh

Didokumentasikan bersama WebToApk.__dlBegin - contoh di sana menunjukkan yang ini juga.

Jalur internal di balik <a download>, unduhan Blob, window.open(blobUrl) dan AppMint.saveBlob / AppMint.downloadFile: file dialirkan dari halaman ke aplikasi dalam irisan, sehingga ekspor besar tidak pernah menjadi satu string raksasa di memori. Halaman memakai kode unduhan standar; jangan panggil ini langsung, karena metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: __dlBegin(name, type, size) mengembalikan token (atau "" jika ponsel tidak punya ruang); __dlChunk(token, base64) mengembalikan false jika sebuah irisan tidak bisa ditulis; __dlEnd(token, action) menyelesaikan (save = Downloads, open = tampilkan, share, copy). Membutuhkan: tidak ada.

Gunakan API publik:

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"

Ekspor gambar dari canvas lewat jalur yang sama:

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

Simpan cadangan JSON yang bisa disimpan pengguna, dengan pesan saat selesai:

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

Catatan: Diimplementasikan di shell aplikasi (ShellDownloads.kt).

__dlBegin jembatan#

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

Membuka unduhan sebesar [size] byte bernama [name] ([type] = tipe MIME-nya) yang akan dialirkan halaman dengan __dlChunk. Mengembalikan token, atau "" jika tidak ada ruang.

Contoh

Jalur internal di balik <a download>, unduhan Blob, window.open(blobUrl) dan AppMint.saveBlob / AppMint.downloadFile: file dialirkan dari halaman ke aplikasi dalam irisan, sehingga ekspor besar tidak pernah menjadi satu string raksasa di memori. Halaman memakai kode unduhan standar; jangan panggil ini langsung, karena metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: __dlBegin(name, type, size) mengembalikan token (atau "" jika ponsel tidak punya ruang); __dlChunk(token, base64) mengembalikan false jika sebuah irisan tidak bisa ditulis; __dlEnd(token, action) menyelesaikan (save = Downloads, open = tampilkan, share, copy). Membutuhkan: tidak ada.

Gunakan API publik:

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"

Ekspor gambar dari canvas lewat jalur yang sama:

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

Simpan cadangan JSON yang bisa disimpan pengguna, dengan pesan saat selesai:

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

Catatan: Diimplementasikan di shell aplikasi (ShellDownloads.kt).

__dlChunk jembatan#

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

Satu irisan base64 dari unduhan [token]; false jika transfer gagal.

Contoh

Didokumentasikan bersama WebToApk.__dlBegin - contoh di sana menunjukkan yang ini juga.

Jalur internal di balik <a download>, unduhan Blob, window.open(blobUrl) dan AppMint.saveBlob / AppMint.downloadFile: file dialirkan dari halaman ke aplikasi dalam irisan, sehingga ekspor besar tidak pernah menjadi satu string raksasa di memori. Halaman memakai kode unduhan standar; jangan panggil ini langsung, karena metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: __dlBegin(name, type, size) mengembalikan token (atau "" jika ponsel tidak punya ruang); __dlChunk(token, base64) mengembalikan false jika sebuah irisan tidak bisa ditulis; __dlEnd(token, action) menyelesaikan (save = Downloads, open = tampilkan, share, copy). Membutuhkan: tidak ada.

Gunakan API publik:

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"

Ekspor gambar dari canvas lewat jalur yang sama:

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

Simpan cadangan JSON yang bisa disimpan pengguna, dengan pesan saat selesai:

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

Catatan: Diimplementasikan di shell aplikasi (ShellDownloads.kt).

__dlEnd jembatan#

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

Menyelesaikan unduhan [token] dan menjalankan tindakannya: [action] "save" (Downloads, dengan bilah "Diunduh · Buka" dan notifikasi), "open" (penampil PDF atau aplikasi ponsel untuk tipe tersebut), "share" (lembar berbagi) atau "copy" (gambar ke clipboard).

Contoh

Didokumentasikan bersama WebToApk.__dlBegin - contoh di sana menunjukkan yang ini juga.

Jalur internal di balik <a download>, unduhan Blob, window.open(blobUrl) dan AppMint.saveBlob / AppMint.downloadFile: file dialirkan dari halaman ke aplikasi dalam irisan, sehingga ekspor besar tidak pernah menjadi satu string raksasa di memori. Halaman memakai kode unduhan standar; jangan panggil ini langsung, karena metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: __dlBegin(name, type, size) mengembalikan token (atau "" jika ponsel tidak punya ruang); __dlChunk(token, base64) mengembalikan false jika sebuah irisan tidak bisa ditulis; __dlEnd(token, action) menyelesaikan (save = Downloads, open = tampilkan, share, copy). Membutuhkan: tidak ada.

Gunakan API publik:

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"

Ekspor gambar dari canvas lewat jalur yang sama:

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

Simpan cadangan JSON yang bisa disimpan pengguna, dengan pesan saat selesai:

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

Catatan: Diimplementasikan di shell aplikasi (ShellDownloads.kt).

__fsChild jembatan#

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

getFileHandle / getDirectoryHandle (dengan `create`).

Contoh

Jalur internal di balik getFileHandle(name, {create}) dan getDirectoryHandle(name, {create}). Halaman memakai API standar; jangan panggil ini langsung - metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok, kind, name, uri, canWrite} atau {ok:false, error} (NotFoundError, TypeMismatchError, NoModificationAllowedError). Membutuhkan: folder dari showDirectoryPicker().

Gunakan API publik:

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

Periksa apakah sebuah file sudah ada di folder sebelum membuatnya:

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

Simpan pengaturan aplikasi sebagai JSON ke sub-folder "config", buat keduanya jika perlu:

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)

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsFile jembatan#

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

getFile(): `{ok, name, type, size, lastModified, url}` (isinya dialirkan dari url).

Contoh

Transport internal di balik FileSystemFileHandle.getFile(). Halaman memakai API standar; jangan pernah memanggil ini langsung - metode __ dapat berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok, name, type, size, lastModified, url} - isinya dialirkan dari url (origin aplikasi sendiri, dengan range) - atau {ok:false, error} (NotFoundError, TypeMismatchError, foreign_origin). Membutuhkan: handle dari salah satu pemilih.

Gunakan API publik:

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

Tampilkan gambar terpilih di halaman tanpa membacanya ke dalam 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 + ')';
});

Baca ulang file yang Anda buka sebelumnya dan ketahui jika file itu berubah atau dihapus:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsList jembatan#

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

Entri sebuah directory handle: `{ok, entries:[{kind, name, uri, canWrite}]}`.

Contoh

Jalur internal di balik iterasi sebuah FileSystemDirectoryHandle (entries(), keys(), values(), for await). Halaman memakai API standar; jangan panggil ini langsung - metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok, entries:[{kind, name, uri, canWrite}]} atau {ok:false, error}. Membutuhkan: folder dari showDirectoryPicker().

Gunakan API publik:

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

Hitung file di sebuah folder dan semua sub-foldernya:

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

Buat daftar foto terurut dari folder yang dipilih pengguna:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsPermission jembatan#

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

queryPermission() / requestPermission(): `{ok, state:'granted'|'prompt'}` untuk handle yang root pilihannya adalah [rootUri] - 'granted' selama aplikasi memegang izin pilihan itu, melewati peluncuran ulang (izinnya disimpan permanen; handle kembali dari IndexedDB).

Contoh

Jalur internal di balik FileSystemHandle.queryPermission() dan requestPermission(). Halaman memakai API standar; jangan panggil ini langsung - metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok:true, state:'granted'} selama aplikasi memegang izin untuk folder atau file terpilih tempat handle berasal (dipertahankan setelah dimulai ulang), {ok:true, state:'prompt'} setelah izin itu hilang, atau {ok:false, error:'foreign_origin'}. File hanya-baca menjawab 'denied' untuk {mode:'readwrite'} di halaman, tanpa pemanggilan. Membutuhkan: handle dari salah satu pemilih, termasuk yang dibaca kembali dari IndexedDB.

Gunakan API publik:

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

Minta lagi dari ketukan saat izin hilang - pemilih folder terbuka di folder tersebut, dan hanya folder yang sama yang diberi izin:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt). Lihat showOpenFilePicker untuk seluruh File System Access API.

__fsPick jembatan#

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

Membuka pemilih [kind] (open | save | directory); handle tiba sebagai `appmint:fs`.

Contoh

Jalur internal di balik showOpenFilePicker(), showSaveFilePicker() dan showDirectoryPicker(). Halaman memakai API standar; jangan panggil ini langsung - metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: {ok:true} setelah pemilih tampil di layar, atau {ok:false, error} (NotAllowedError jika sudah ada yang terbuka, foreign_origin). Handle tiba sebagai event appmint:fs {requestId, handles:[{kind, name, uri, canWrite}]} atau {requestId, error:'cancelled'}. Membutuhkan: tidak ada yang perlu diaktifkan.

Gunakan API publik:

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

Simpan laporan dengan nama yang disarankan, dari ketukan tombol (pemilih butuh gestur pengguna):

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

Biarkan pengguna memilih beberapa foto sekaligus, dimulai dari folder Pictures:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt). Kode halaman yang sama berfungsi tanpa perubahan di Chrome desktop.

__fsRemove jembatan#

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

removeEntry(name, {recursive}).

Contoh

Transport internal di balik FileSystemDirectoryHandle.removeEntry(name, {recursive}). Halaman memakai API standar; jangan pernah memanggil ini langsung - metode __ dapat berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok:true} atau {ok:false, error} (NotFoundError, InvalidModificationError untuk folder tidak kosong tanpa recursive). Membutuhkan: folder dari showDirectoryPicker().

Gunakan API publik:

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

Hapus satu file dan anggap "sudah hilang" sebagai selesai:

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

Hapus folder, bertanya dulu hanya bila masih ada isinya:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsWritableAbort jembatan#

window.WebToApk.__fsWritableAbort(token: String)

abort(): penukaran dibatalkan; dokumen tidak tersentuh.

Contoh

Jalur internal di balik FileSystemWritableFileStream.abort() - salinan swap dibuang dan file tetap seperti semula. Halaman memakai API standar; jangan panggil ini langsung - metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: tidak ada. Membutuhkan: stream tulis yang terbuka.

Gunakan API publik:

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

Biarkan pengguna membatalkan ekspor panjang; tidak ada yang setengah tertulis masuk ke file:

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

Unduh langsung ke file dengan pipeTo; menghentikan unduhan membatalkan penulisan:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsWritableClose jembatan#

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

close(): swap menggantikan dokumen.

Contoh

Jalur internal di balik FileSystemWritableFileStream.close() - salinan swap menggantikan file. Halaman memakai API standar; jangan panggil ini langsung - metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok:true} atau {ok:false, error} (NoModificationAllowedError, InvalidModificationError bila aplikasi file tidak dapat memperpendeknya). Membutuhkan: stream yang dapat ditulis yang terbuka.

Gunakan API publik:

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

Beri tahu pengguna mengapa penyimpanan tidak berhasil (close() adalah tempat yang bisa gagal):

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

Tulis file dalam beberapa bagian; pembaca tetap melihat isi lama sampai 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');
}

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsWritableOpen jembatan#

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

createWritable(): file swap, disalin dari file bila [keepExisting].

Contoh

Transport internal di balik FileSystemFileHandle.createWritable({keepExistingData}). Halaman memakai API standar; jangan pernah memanggil ini langsung - metode __ dapat berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok, token} untuk salinan swap pribadi, atau {ok:false, error} (NoModificationAllowedError untuk file hanya-baca). Membutuhkan: handle dari salah satu pemilih.

Gunakan API publik:

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

Tambahkan satu baris ke akhir file log yang sudah ada, tanpa menghapus isinya:

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
}

Simpan canvas sebagai PNG, dan biarkan file tidak tersentuh jika terjadi masalah:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsWritableTruncate jembatan#

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

truncate(size) pada stream tulis.

Contoh

Transport internal di balik FileSystemWritableFileStream.truncate(size). Halaman memakai API standar; jangan pernah memanggil ini langsung - metode __ dapat berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok:true} atau {ok:false, error}. Membutuhkan: stream yang dapat ditulis yang terbuka.

Gunakan API publik:

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

Simpan hanya 1.000 byte pertama sebuah file dan potong semua yang setelahnya:

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

Pesan ruang: memotong ke ukuran yang lebih besar akan memperbesar file, dan byte baru terbaca sebagai nol:

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt).

__fsWritableWrite jembatan#

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

Satu potongan dari write() sebuah stream tulis, pada [position].

Contoh

Jalur internal di balik FileSystemWritableFileStream.write() - satu potongan pada suatu posisi di salinan swap. Halaman memakai API standar; jangan panggil ini langsung - metode __ bisa berubah tanpa pemberitahuan.

Mengembalikan: JSON {ok:true} atau {ok:false, error}. Membutuhkan: stream yang dapat ditulis yang terbuka.

Gunakan API publik:

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

Tambahkan satu baris ke akhir file log yang dipilih pengguna (pemilih file butuh ketukan):

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

Simpan gambar canvas sebagai PNG baru dengan "Simpan sebagai" (Blob ditulis sebagai byte mentahnya):

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

Catatan: Diimplementasikan di shell aplikasi (FileSystemAccess.kt). Data besar dikirim dalam chunk.

getMaxIoChunkSize jembatan#

window.WebToApk.getMaxIoChunkSize(): Long

Batas byte per panggilan untuk [readFileBase64] / [writeFileBase64], untuk perhitungan potongan.

Contoh

Batas byte per panggilan untuk readFileBase64 / writeFileBase64 (4 MB). Halaman biasanya memakai WebToApkFS.maxChunkSize().

Mengembalikan: (sinkron) sebuah angka (4194304). Membutuhkan: tidak ada untuk memanggilnya; metode baca/tulis yang dijelaskannya membutuhkan Native Folder Access (SAF) dicentang di Langkah 4 (Akses).

Cara publik:

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

Panggilan mentah:

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

Hitung berapa banyak panggilan jembatan yang dibutuhkan sebuah file, untuk menentukan ukuran bilah kemajuan sebelum menyalinnya:

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

Salin satu file ke file lain, satu potongan berukuran maksimum setiap kali:

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)

Catatan: Penulisan yang lebih besar dari ini ditolak dengan chunk too large (max N bytes); pembacaan yang lebih besar dipotong sesuai ukuran ini.

pickFile jembatan#

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

Membuka pemilih satu file sistem (ACTION_OPEN_DOCUMENT). Tidak seperti [requestFolderAccess], ini memberi akses ke tepat satu file, yang semestinya diminta oleh editor atau penghancur file; izin baca/tulisnya disimpan permanen sehingga URI tetap berfungsi setelah proses dihentikan. Jawabannya tiba di `WebToApkOnFilePickResult(success, fileJson, requestId)`; `error` pada kegagalan adalah "cancelled", "disabled", "no-picker", "foreign_origin", atau "busy" (sebuah pemilih sudah terbuka untuk permintaan sebelumnya, yang tetap memegang jawabannya sendiri). @param mimeFilter mis. "image/\*", "application/pdf", atau "" untuk file apa pun. (Tanda bintang di-escape dengan backslash hanya karena komentar blok Kotlin bersarang.)

Contoh

Membuka pemilih sistem untuk SATU file. Halaman biasanya memakai helper Promise WebToApkFS.pickFile(mimeFilter); metode mentah ini menjawab lewat callback.

Mengembalikan: tidak ada sekarang. Nanti shell memanggil window.WebToApkOnFilePickResult(success, fileJson, requestId), dengan fileJson berupa string JSON {ok, uri, name, size, mime} jika berhasil atau {ok:false, uri:'', error} dengan error = 'cancelled', 'disabled', 'no-picker', 'busy' atau 'foreign_origin'. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Cara publik (disarankan):

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

Panggilan mentah, dicocokkan sendiri 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
  });
}

Lampirkan PDF dan tampilkan nama serta ukurannya, bedakan pembatalan dari masalah sungguhan:

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

Buka file teks dan muat isinya ke 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
}

Catatan: Hanya satu pemilihan yang dapat terbuka sekaligus: panggilan yang dibuat saat satu terbuka langsung dijawab dengan error:'busy', dan yang terbuka tetap mendapat jawabannya sendiri. Izin disimpan (baca dan tulis) sampai Anda memanggil releaseFileAccess(uri). File tidak dibaca di sini - gunakan readFileBase64 / WebToApkFS.readBytes dengan uri.

readFileBase64 jembatan#

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

Membaca hingga [length] byte mentah mulai dari [offset] dan mengembalikannya sebagai base64. @param pathOrUri URI `content://` dari [pickFile], atau path relatif terhadap folder yang diizinkan oleh [requestFolderAccess]. @param length byte yang dibaca; dibatasi 4 MB per panggilan. 0 atau negatif berarti "sebanyak yang diizinkan satu panggilan". @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` memberi tahu loop chunking kapan berhenti tanpa perlu membandingkan dengan `size` sendiri.

Contoh

Membaca byte mentah dari file pada offset byte tertentu dan mengembalikannya sebagai base64. Halaman biasanya memakai WebToApkFS.readBytes(pathOrUri, offset, length) (hasil sama, sudah diurai) atau WebToApkFS.readAll(pathOrUri).

Mengembalikan: (sinkron) string JSON {ok:true, base64, bytesRead, offset, size, eof} atau {ok:false, error}. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Cara publik:

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

Panggilan mentah - perhatikan hasilnya adalah string yang harus Anda JSON.parse, bukan base64 polos:

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

Periksa apakah file terpilih benar-benar PNG dengan hanya membaca 8 byte pertamanya:

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

Baca file besar potongan demi potongan dengan bilah kemajuan, tanpa pernah menyimpan semuanya di memori:

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
}

Catatan: pathOrUri adalah URI content:// dari pickFile atau path relatif terhadap folder dari requestFolderAccess. Paling banyak 4 MB per panggilan (getMaxIoChunkSize()); ulangi pada bytesRead sampai eof. Kesalahan: not-found, disabled, cannot-open, offset must be >= 0, offset past end of file (N bytes), foreign_origin (halaman dari situs selain milik aplikasi sendiri).

releaseFileAccess jembatan#

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

Melepaskan izin satu file yang disimpan permanen oleh [pickFile]. Android membatasi jumlah izin URI yang boleh disimpan aplikasi, jadi aplikasi yang memilih banyak file sebaiknya melepas yang sudah selesai.

Contoh

Melepaskan izin baca/tulis yang tersimpan untuk file yang dipilih dengan pickFile. Halaman biasanya memakai WebToApkFS.releaseFile(uri), yang mengembalikan boolean sungguhan.

Mengembalikan: (sinkron) string "true" atau "false". Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build (selain itu selalu "false").

Cara publik:

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

Panggilan mentah:

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

Simpan daftar "file terbaru" paling banyak 10 dan lepaskan izin file mana pun yang tergeser dari daftar:

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

Tutup dokumen saat ini: lepaskan izinnya dan lupakan URI yang tersimpan:

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

Catatan: Android membatasi berapa banyak izin file yang boleh disimpan aplikasi, jadi lepaskan file yang tidak lagi Anda butuhkan. File itu sendiri tidak disentuh.

statFile jembatan#

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

Metadata file atau folder tanpa membaca isinya. Path relatif menyebut file atau sub-folder dari folder yang diizinkan; folder menjawab `isDirectory: true` dengan `size` 0 dan `mime` `vnd.android.document/directory`. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, atau `{ok:false, error}` dengan `not-found`, `disabled` atau `foreign_origin`.

Contoh

Membaca metadata file (nama, ukuran, tipe) tanpa membaca byte-nya. Halaman biasanya memakai WebToApkFS.stat(pathOrUri), yang mengurai JSON untuk Anda.

Mengembalikan: (sinkron) string JSON {ok, name, size, mime, lastModified, uri, isDirectory, canWrite}, atau {ok:false, error} ('not-found', 'disabled', 'foreign_origin'). Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Cara publik:

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

Panggilan mentah:

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

Tolak file yang terlalu besar sebelum membaca satu byte pun:

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

Cari sub-folder dari folder yang diizinkan dan hanya tawarkan "Simpan di sini" jika bisa ditulis:

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

Catatan: Menerima URI content:// dari pickFile atau path relatif terhadap folder yang diizinkan dengan requestFolderAccess. Ini hanya menjawab halaman milik aplikasi sendiri atau situs web yang dikonfigurasi; halaman dari situs lain mendapat foreign_origin.

writeFileBase64 jembatan#

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

Menulis byte mentah dan memastikan berapa banyak yang masuk. @param mode `"patch"` menulis di [offset] dengan membiarkan sisa file utuh (yang dibutuhkan penimpaan multi-pass), `"truncate"` menggantikan file, `"append"` mengabaikan [offset] dan menulis di akhir. @return JSON `{ok, bytesWritten, offset, size, error}` - `size` dibaca ulang dari file setelah fsync, sehingga pemanggil dapat memverifikasi penulisan sungguhan, bukan memercayai boolean.

Contoh

Menulis byte base64 ke file pada suatu offset (patch), menimpa seluruh file (truncate), atau di akhir (append). Halaman biasanya memakai WebToApkFS.writeBytes(pathOrUri, base64, offset, mode) atau WebToApkFS.writeAll(pathOrUri, base64).

Mengembalikan: (sinkron) string JSON {ok, bytesWritten, offset, size} (size dibaca ulang setelah fsync) atau {ok:false, error}. Membutuhkan: centang Native Folder Access (SAF) di Langkah 4 (Akses) saat Anda membuat build.

Cara publik:

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

Panggilan mentah:

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

Simpan gambar canvas sebagai PNG ke file yang dipilih pengguna, menggantikan isi sebelumnya:

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

Timpa file di tempat dengan nol sebelum menghapusnya, dan pastikan setiap putaran berhasil:

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
}

Catatan: Mode adalah 'patch' (default bila kosong), 'truncate' atau 'append'. Satu panggilan paling banyak 4 MB byte yang didekode (getMaxIoChunkSize()). Path relatif dibuat di dalam folder yang diizinkan bila belum ada. Kesalahan: disabled, foreign_origin (halaman di layar bukan milik aplikasi sendiri), bad mode: x, offset must be >= 0, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

Dibuat dari runtime aplikasi dan file contohnya pada setiap build dokumentasi. Baca sebagai Markdown · Semua keluarga.

Diperiksa terhadap jembatan yang dirilis pada 2026-10-22.