Créer un APK gratuit

API du pont JavaScript

Fichiers : un fichier à la fois

Le sélecteur système et la lecture/écriture binaire par blocs, pour les fichiers trop gros pour tenir dans une chaîne.

AppMint.downloadFile assistant#

AppMint.downloadFile(data, fileName, mimeType?)

Exemple

Enregistre un fichier créé par votre page - à partir d'une chaîne base64, d'une URL data:, d'une URL blob: ou d'un Blob - avec le nom de fichier de votre choix. La feuille « Enregistrer sous… » d'Android s'ouvre avec ce nom déjà rempli.

Renvoie : (sync) true une fois l'enregistrement transmis à Android (false seulement pour une URL data: qu'il n'a pas pu analyser). Lève TypeError si data n'est ni un Blob ni une chaîne. L'utilisateur choisit toujours le dossier, et peut annuler. Nécessite : rien - toujours présent dans l'application.

Chaîne Base64 plus un type 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();
  }
}

Une URL data: (le type MIME vient de l'URL) ou un Blob :

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

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

Un PDF en base64 que votre serveur renvoie en JSON ; un nom sans extension reçoit .pdf d'après le type 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
}

Une URL blob: que vous avez déjà créée pour un aperçu à l'écran :

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

Remarques : Seule la dernière partie de fileName est utilisée (pas de dossiers). Un nom sans extension reçoit une extension d'après le type MIME. Le troisième argument n'est utilisé que pour une simple chaîne base64. Si le téléphone n'a aucun écran « Enregistrer sous… », le fichier est écrit directement dans Téléchargements. Votre code <a download> habituel fonctionne aussi - voir downloads.

AppMint.saveBlob assistant#

AppMint.saveBlob(blob, fileName)

Exemple

Enregistre un Blob (ou un File) avec le nom de votre choix. La feuille « Enregistrer sous… » d'Android s'ouvre avec ce nom déjà rempli.

Renvoie : (sync) true une fois l'enregistrement transmis à Android. Lève TypeError quand le premier argument n'est pas un Blob. Nécessite : rien - toujours présent dans l'application.

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

Un File apporte son propre nom, on peut donc omettre le nom :

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

Un fichier de votre serveur nécessitant un en-tête de connexion, enregistré sous un nom de votre choix :

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

Plusieurs fichiers regroupés dans un seul ZIP (ici avec JSZip) et enregistrés d'un coup :

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

Remarques : Le Blob est lu en mémoire et envoyé à Android d'un seul bloc ; les très gros blobs (des centaines de Mo) peuvent donc manquer de mémoire. L'utilisateur choisit le dossier et peut annuler ; rien n'indique à la page lequel des deux s'est produit.

AppMint.setDownloadName assistant#

AppMint.setDownloadName(url, fileName)

Exemple

Donne à une URL blob: ou data: le nom de fichier sous lequel elle doit être enregistrée, avant que votre code (ou une bibliothèque) ne déclenche le téléchargement.

Renvoie : rien. Nécessite : rien - toujours présent dans l'application.

Utile quand une bibliothèque télécharge une URL que vous avez construite plus tôt et que vous ne pouvez pas lui passer de nom :

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

Un PDF que votre page ouvre avec window.open va dans la visionneuse PDF du téléphone ; donnez-lui d'abord un nom pour qu'il ne s'appelle pas « 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');
}

Une URL data: fonctionne de la même façon, par exemple l'image d'un graphique vers laquelle une bibliothèque pointe sans attribut 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';

Remarques : Le nom est mémorisé même après URL.revokeObjectURL(url), car certaines bibliothèques révoquent l'URL avant la fin de l'enregistrement. Vous en aurez rarement besoin : <a download="name">, URL.createObjectURL(file) (un File a un nom) et le saveAs(blob, name) de FileSaver sont tous pris en charge automatiquement. Le nom 'download' est ignoré comme « sans nom ».

WebToApkFS.maxChunkSize assistant#

WebToApkFS.maxChunkSize()

Exemple

Indique le maximum d'octets qu'un appel readBytes ou writeBytes peut déplacer (4 Mo), pour dimensionner votre boucle de blocs.

Retour : (sync) un nombre d'octets, ou 0 quand la méthode du pont est absente. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation (c'est ce qui injecte 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 };
  }
}

Afficher la progression de la lecture : la taille des blocs indique combien d'étapes prend un fichier :

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

Ajouter un grand tableau d'octets à un fichier du dossier autorisé, un morceau autorisé à la fois :

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

Remarques : Un bloc plus petit garde la page plus réactive (chaque appel est synchrone) ; la limite indique seulement la taille maximale d'un appel.

WebToApkFS.pickFile assistant#

WebToApkFS.pickFile(mimeFilter)

Exemple

Ouvre le sélecteur de fichiers Android pour que l'utilisateur choisisse UN fichier ; votre page peut ensuite lire et écrire les vrais octets de ce fichier, même après le redémarrage de l'application.

Renvoie : Promise<{ok, uri, name, size, mime, error}>. En cas de succès, ok vaut true et uri est une adresse content:// à passer à stat, readBytes, writeBytes. En cas d'échec, ok vaut false et error est 'cancelled', 'disabled', 'no-picker', 'busy' (un sélecteur est déjà ouvert pour un appel précédent, qui garde sa propre réponse), 'foreign_origin' ou 'unsupported' (aucune application). Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

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
}

Réutiliser plus tard le uri enregistré, sans redemander à l'utilisateur :

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

Le filtre n'accepte QU'UN motif, donc choisissez large et vérifiez le nom vous-même :

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

Remarques : Le filtre est un seul motif de type MIME ('image/*', 'text/csv'), pas une liste. Le sélecteur demande l'accès en lecture ET en écriture et le conserve. Android limite le nombre de fichiers qu'une application peut conserver ainsi, appelez donc WebToApkFS.releaseFile(uri) quand vous avez fini avec un fichier. Pour un dossier entier, utilisez plutôt WebToApkFS.requestAccess(). WebToApkFS est installé quand Native Folder Access ou Secure Keys est coché ; ses appels ne fonctionnent qu'avec Native Folder Access (WebToApkFS.isAvailable()).

WebToApkFS.readAll assistant#

WebToApkFS.readAll(pathOrUri)

Exemple

Lit un fichier entier dans une seule chaîne base64. Il boucle sur readBytes pour vous, 4 Mo à la fois.

Renvoie : (sync) {ok:true, base64, size}, ou le {ok:false, error} du premier bloc en échec. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Charger une image choisie dans un <img>:

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

Lire un fichier texte UTF-8 (fonctionne pour toutes les langues, pas seulement l'anglais) :

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

Afficher la somme de contrôle SHA-256 d'un fichier, par exemple pour comparer un téléchargement avec celle publiée :

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

Remarques : Le fichier entier se retrouve en mémoire (en octets puis en base64), et l'appel bloque la page pendant son exécution. Utilisez-le pour des fichiers qui tiennent facilement en mémoire ; pour les gros fichiers, bouclez sur WebToApkFS.readBytes et traitez chaque bloc. Pour du texte simple dans le dossier autorisé, WebToApkFS.readText(path) est plus simple.

WebToApkFS.readBytes assistant#

WebToApkFS.readBytes(pathOrUri, offset, length)

Exemple

Lit les vrais octets d'un fichier, à partir d'une position en octets, en base64. Sûr pour les images, PDF, ZIP - rien n'est modifié par un décodage texte.

Renvoie : (sync) {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'). Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Lire les 64 premiers Ko, par exemple pour vérifier l'en-tête d'un fichier :

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

Lire un gros fichier morceau par morceau, pour qu'il n'ait jamais à tenir en mémoire d'un coup :

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

Lire seulement la fin d'un fichier, par exemple le tag de 128 octets à la fin d'un MP3 :

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

Remarques : Un appel lit au plus WebToApkFS.maxChunkSize() octets (4 Mo) ; length à 0 signifie « autant qu'un appel le permet ». Bouclez sur bytesRead et arrêtez-vous à eof. L'appel est synchrone, gardez donc des blocs modérés sur le thread de l'interface. Pour les petits fichiers, WebToApkFS.readAll(uri) fait la boucle pour vous.

WebToApkFS.readSealed assistant#

WebToApkFS.readSealed(pathOrUri, alias)

Exemple

Lit un fichier écrit par WebToApkFS.writeSealed et le déchiffre avec la même clé du keystore.

Retour : (sync) {ok:true, base64} (les octets d'origine), ou {ok:false, error} - 'keystore-disabled', 'corrupt-sealed-file', un code du keystore ('no-such-key' après suppression de la clé, 'auth-required', 'key-invalidated') ou une erreur de fichier ('not-found' …). Nécessite : cochez Native Folder Access (SAF) et Clés sécurisées (chiffrement) à l'étape 4 (Accès) lors de la compilation.

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

Garder les paramètres de l'application dans un fichier JSON scellé, avec des valeurs par défaut avant le premier enregistrement :

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

Déplacer un fichier scellé vers une nouvelle clé, puis détruire l'ancienne clé :

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

Remarques : Elle échoue définitivement une fois que AppMintKeystore.deleteKey(alias) a été exécuté - c'est le but. Le fichier entier est lu et déchiffré en mémoire, de façon synchrone ; gardez les fichiers scellés petits (notes, clés, jetons).

WebToApkFS.releaseFile assistant#

WebToApkFS.releaseFile(uri)

Exemple

Rend l'accès à long terme que votre application a obtenu quand l'utilisateur a choisi un fichier avec WebToApkFS.pickFile.

Renvoie : (sync) true quand l'autorisation a été libérée, false sinon (aucune autorisation, URI invalide, hors de l'application). Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

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
}

Quand l'utilisateur choisit un nouveau fichier par défaut, rendez l'autorisation de l'ancien :

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

Un bouton « Oublier les fichiers récents » qui libère tous les fichiers mémorisés par l'application :

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

Remarques : Android limite le nombre de fichiers choisis auxquels une application peut conserver l'accès. Une application qui choisit beaucoup de fichiers (un importateur, une visionneuse) devrait libérer chacun d'eux une fois terminé. Après la libération, stat / readBytes / writeBytes sur cet URI répondent not-found. Cela ne supprime pas le fichier.

WebToApkFS.stat assistant#

WebToApkFS.stat(pathOrUri)

Exemple

Vous indique le nom, la taille, le type et la date d'un fichier sans lire son contenu.

Renvoie : (sync) {ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}, ou {ok:false, error} avec error = 'not-found', 'disabled', 'foreign_origin' ou 'unsupported'. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

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

Un chemin dans le dossier que l'utilisateur a autorisé avec WebToApkFS.requestAccess() fonctionne aussi :

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

Réimporter un fichier mémorisé uniquement s'il a changé depuis la dernière importation :

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

Remarques : pathOrUri est un URI content:// provenant de pickFile() ou un chemin relatif au dossier autorisé. Un URI que votre application n'a jamais reçu renvoie not-found. lastModified est exprimé en millisecondes depuis 1970 (0 si le fournisseur ne l'indique pas).

WebToApkFS.writeAll assistant#

WebToApkFS.writeAll(pathOrUri, base64)

Exemple

Remplace tout le contenu d'un fichier par vos octets. Il découpe les données en morceaux de 4 Mo pour vous : le premier morceau tronque le fichier, les suivants sont ajoutés à la suite.

Renvoie : (sync) {ok:true, bytesWritten}, ou le {ok:false, error} du premier morceau en échec. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Réenregistrer du texte UTF-8 (toutes langues) dans un fichier choisi par l'utilisateur :

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

Écrire une image de canvas dans le dossier autorisé (le fichier est créé s'il manque) :

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

Enregistrer une sauvegarde JSON dans le dossier autorisé, en demandant d'abord un dossier s'il n'y en a pas, puis confirmer sa taille sur le disque :

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

Remarques : Toute l'entrée reste en mémoire pendant l'écriture, et l'appel bloque la page. Si un morceau ultérieur échoue, le fichier contient déjà les morceaux précédents - vérifiez ok et prévenez l'utilisateur. Pour garder un fichier secret, utilisez plutôt WebToApkFS.writeSealed.

WebToApkFS.writeBytes assistant#

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

Exemple

Écrit de vrais octets dans un fichier et vous dit combien ont été écrits. Peut modifier des octets au milieu, remplacer tout le fichier ou ajouter à la fin.

Renvoie : (sync) {ok, bytesWritten, offset, size} - size est relu depuis le fichier après l'écriture des données sur le disque - ou {ok:false, error}. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Patch - modifier 4 octets à la position 100, garder le reste du fichier :

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 - remplacer tout le fichier (le décalage est ignoré) :

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 - ajouter à la fin (le décalage est ignoré) :

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

Remarques : mode vaut 'patch' (par défaut), 'truncate' ou 'append'. Au plus 4 Mo par appel (WebToApkFS.maxChunkSize()) ; pour des données plus grosses, utilisez WebToApkFS.writeAll. Un chemin à l'intérieur du dossier autorisé est créé s'il n'existe pas ; un URI content:// choisi doit être un URI que l'utilisateur a choisi. btoa n'accepte que du texte Latin-1 - pour tout autre texte, encodez d'abord en octets (voir WebToApkFS.writeAll). Erreurs : disabled, foreign_origin (la page affichée n'est pas celle de l'application), bad mode: x, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

WebToApkFS.writeSealed assistant#

WebToApkFS.writeSealed(pathOrUri, base64, alias)

Exemple

Écrit un fichier chiffré avec une clé matérielle provenant de AppMintKeystore. Supprimer cette clé plus tard rend le fichier illisible pour tout le monde - la seule vraie « suppression sécurisée » sur un téléphone.

Renvoie : (sync) {ok:true, bytesWritten} (taille chiffrée sur le disque), ou {ok:false, error} - 'keystore-disabled', un code de keystore tel que 'no-such-key', 'auth-required', 'key-invalidated', ou une erreur de fichier. Nécessite : cochez Native Folder Access (SAF) et Secure Keys (encryption) à l'étape 4 (Accès) lors de la compilation.

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

La supprimer pour de bon - détruire la clé, puis supprimer le fichier :

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

Scellez une photo choisie par l'utilisateur, en octets bruts, dans le dossier qu'il a autorisé :

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

Une clé de journal qui ne fonctionne que pendant 5 minutes après le déverrouillage du téléphone, avec chaque erreur expliquée :

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

Remarques : N'« écrasez pas avec des zéros » pour effacer : le stockage flash du téléphone conserve les anciens blocs. Le format de fichier est une liste de morceaux AES-GCM préfixés par leur longueur ; ne le relisez qu'avec WebToApkFS.readSealed et le même alias. Une clé créée avec {requireAuth:true} répond auth-required tant que l'utilisateur ne s'est pas déverrouillé récemment.

downloads standard web#

<a download> and Blob downloads

Exemple

Votre code de téléchargement web habituel fonctionne dans l'application, comme dans Chrome. Un WebView ignore de lui-même les téléchargements blob: et data: ; l'application les intercepte et enregistre le fichier dans le dossier Téléchargements du téléphone sous votre nom de fichier, puis affiche « Téléchargé <nom> · Ouvrir » et une notification « Téléchargement terminé ».

Renvoie : rien à la page. Nécessite : rien à activer. Les téléchargements par adresse web (https://…) vont aussi dans le dossier Téléchargements (un PDF ou une image demande d'abord « Afficher ou Télécharger »), tout comme les fichiers intégrés à l'application (<a href="manual.pdf" download>) ; sur Android 9 et versions antérieures, l'application demande d'abord l'autorisation de stockage. Les gros fichiers ne posent pas de problème : le fichier est transmis à l'application par morceaux.

<a download> avec un Blob - le schéma le plus courant :

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

Une URL data:, FileSaver.js et navigator.msSaveBlob fonctionnent aussi :

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 d'une URL blob: ou data: signifie « l'afficher », pas « l'enregistrer » : un PDF s'ouvre dans la visionneuse PDF de l'application, les autres types dans l'application du téléphone prévue pour ce type :

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

Remarques : Sans nom, le fichier est enregistré sous download.<ext> ; un second fichier du même nom devient name (1).ext. Un objet File apporte son propre nom (URL.createObjectURL(file)). <a href="/files/x" download="Name.pdf"> sur votre propre site conserve Name.pdf (Chrome ignore l'attribut sur les liens d'autres sites, et l'application aussi). Si aucune application du téléphone ne peut afficher un fichier window.open, il est enregistré dans Téléchargements à la place. Pour laisser l'utilisateur choisir le dossier, utilisez showSaveFilePicker(). Pour enregistrer sans élément <a>, utilisez AppMint.saveBlob(blob, name) ou AppMint.downloadFile(base64, name, mime).

input-file standard web#

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

Exemple

Un champ de fichier normal fonctionne dans l'application comme dans Chrome : il ouvre le sélecteur de photos, l'appareil photo ou le sélecteur de fichiers, et votre page reçoit des objets File ordinaires.

Renvoie : l'événement change du champ avec input.files (vide quand l'utilisateur annule). Nécessite : les photos et les fichiers ne nécessitent rien. Pour capture (l'appareil photo), cochez Camera à l'étape 4 (Accès) lors de la compilation ; l'application demande l'autorisation de l'appareil photo la première fois.

Photos - ouvre le sélecteur de photos d'Android (aucune demande d'autorisation ; la page ne reçoit que ce que l'utilisateur a choisi) :

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

Appareil photo - capture ignore le sélecteur et ouvre directement l'appareil photo :

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

N'importe quel fichier, ou plusieurs types (les extensions fonctionnent aussi) :

<input type="file" accept=".pdf,.csv,application/json" multiple>
Vous écrivezCe qui s'ouvre
accept="image/*" / "video/*" / "image/*,video/*"le sélecteur de photos (photos, vidéos ou les deux)
ajouter multiplel'utilisateur peut en choisir plusieurs
ajouter capture (avec Camera coché)directement l'appareil photo - photo pour image/*, vidéo pour video/*
accept="image/*,.pdf" ou non acceptle sélecteur de fichiers du système

Remarques : Sans Camera coché, capture est ignoré et le sélecteur s'ouvre. Quand Native Folder Access (SAF) est coché, l'application affiche à la place son propre menu « Select Upload Source » : Take Photo / Record Video (si Camera est coché), Choose Files et Upload Folder (envoie tous les fichiers d'un dossier). Avec Storage coché sur Android 12 et versions antérieures, choisir un fichier qui n'est pas une photo peut d'abord demander l'autorisation de stockage. Les téléphones sans sélecteur de photos utilisent le sélecteur de fichiers, qui affiche les mêmes photos.

showOpenFilePicker standard web#

showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access

Exemple

L'API standard File System Access fonctionne dans l'application, au-dessus du sélecteur de documents propre à Android : ouvrir un fichier et réenregistrer DANS CE FICHIER, créer un fichier avec une feuille « Enregistrer sous », ou obtenir un dossier entier pour y lire, créer et supprimer des fichiers.

Renvoie : showOpenFilePicker(options) → une Promise d'un tableau de FileSystemFileHandle ; showSaveFilePicker(options) → un FileSystemFileHandle ; showDirectoryPicker(options) → un FileSystemDirectoryHandle. Un sélecteur annulé rejette AbortError ; l'absence de geste de l'utilisateur rejette SecurityError ; un second sélecteur alors qu'un autre est ouvert rejette NotAllowedError. Nécessite : rien à activer et aucune autorisation - c'est l'utilisateur qui choisit.

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

Choisir plusieurs photos à la fois, en partant du dossier Images :

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

Ajouter une ligne à la fin d'un fichier journal sans réécrire ce qui s'y trouve déjà :

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

Choisissez un dossier de sauvegarde UNE seule fois et enregistrez-y automatiquement à chaque lancement suivant - conservez le handle dans IndexedDB, exactement comme dans Chrome. L'application garde l'autorisation du dossier entre les redémarrages, donc queryPermission() répond 'granted' et la sauvegarde ne demande ni appui ni sélecteur :

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

Si l'autorisation a été retirée (l'utilisateur a effacé l'accès de l'application ou supprimé le dossier), queryPermission() répond 'prompt'. Depuis un appui sur un bouton, requestPermission() rouvre le sélecteur de dossier SUR ce dossier ; choisir le même dossier répond 'granted', tout autre choix (ou l'annulation) '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';
  }
});

Nettoyer un dossier choisi par l'utilisateur : supprimer les anciens fichiers .tmp et les sous-dossiers nommés 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');
});

Remarques : createWritable() suit le modèle de fichier d'échange de la spécification - abort() (ou la disparition de la page) laisse l'original intact ; { keepExistingData: true } part du contenu actuel ; write({type:'seek'|'truncate'|'write', …}), seek() et truncate() fonctionnent. Un fichier que son application propose en lecture seule répond queryPermission({mode:'readwrite'}) avec 'denied' et createWritable() rejette NoModificationAllowedError. getFile() diffuse le document en flux, donc les gros fichiers ne posent pas de problème. Les handles peuvent être stockés dans IndexedDB (y compris dans des objets et des tableaux) et réutilisés après le redémarrage de l'application : l'application conserve l'autorisation Android pour chaque dossier et fichier renvoyé par un sélecteur (les 64 plus récents ; le plus ancien est libéré en premier). queryPermission() répond 'granted' tant que cette autorisation est détenue et 'prompt' une fois qu'elle a disparu ; requestPermission() nécessite alors un appui et rouvre le sélecteur sur le dossier du handle lui-même. Les applications compilées avant le 2026-10-03 perdaient le handle au redémarrage - recompilez, et l'utilisateur choisit à nouveau le dossier une fois. types deviennent le filtre de fichiers du sélecteur. Seule la page de niveau supérieur dispose des sélecteurs. Transports internes : __fsPick (répondu avec l'événement appmint:fs), __fsFile, __fsList, __fsChild, __fsRemove, __fsPermission, __fsWritableOpen/Write/Truncate/Close/Abort. Les applications créées par IA vérifient can('fileHandles').

__dlAbort pont#

window.WebToApk.__dlAbort(token: String)

Abandonne le téléchargement [token] (la lecture par la page a échoué ou elle a renoncé).

Exemple

Documenté avec WebToApk.__dlBegin : l'exemple qui s'y trouve illustre aussi celui-ci.

Transport interne derrière <a download>, les téléchargements de Blob, window.open(blobUrl) et AppMint.saveBlob / AppMint.downloadFile : le fichier est transmis de la page à l'application par tranches, de sorte qu'un gros export n'est jamais une seule chaîne géante en mémoire. Les pages utilisent le code de téléchargement standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.

Renvoie : __dlBegin(name, type, size) renvoie un jeton (ou "" quand le téléphone n'a pas de place) ; __dlChunk(token, base64) renvoie false si une tranche n'a pas pu être écrite ; __dlEnd(token, action) termine (save = Téléchargements, open = l'afficher, share, copy). Nécessite : rien.

Utilisez l'API publique :

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"

Exporter un dessin depuis un canvas par le même chemin :

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

Enregistrer une sauvegarde JSON que l'utilisateur peut conserver, avec un message à la fin :

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

Remarques : Implémenté dans le shell de l'application (ShellDownloads.kt).

__dlBegin pont#

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

Ouvre un téléchargement de [size] octets nommé [name] ([type] = son type MIME) que la page diffusera avec __dlChunk. Renvoie un jeton, ou "" quand il n'y a pas de place.

Exemple

Transport interne derrière <a download>, les téléchargements de Blob, window.open(blobUrl) et AppMint.saveBlob / AppMint.downloadFile : le fichier est transmis de la page à l'application par tranches, de sorte qu'un gros export n'est jamais une seule chaîne géante en mémoire. Les pages utilisent le code de téléchargement standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.

Renvoie : __dlBegin(name, type, size) renvoie un jeton (ou "" quand le téléphone n'a pas de place) ; __dlChunk(token, base64) renvoie false si une tranche n'a pas pu être écrite ; __dlEnd(token, action) termine (save = Téléchargements, open = l'afficher, share, copy). Nécessite : rien.

Utilisez l'API publique :

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"

Exporter un dessin depuis un canvas par le même chemin :

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

Enregistrer une sauvegarde JSON que l'utilisateur peut conserver, avec un message à la fin :

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

Remarques : Implémenté dans le shell de l'application (ShellDownloads.kt).

__dlChunk pont#

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

Une tranche base64 du téléchargement [token] ; false si le transfert a échoué.

Exemple

Documenté avec WebToApk.__dlBegin : l'exemple qui s'y trouve illustre aussi celui-ci.

Transport interne derrière <a download>, les téléchargements de Blob, window.open(blobUrl) et AppMint.saveBlob / AppMint.downloadFile : le fichier est transmis de la page à l'application par tranches, de sorte qu'un gros export n'est jamais une seule chaîne géante en mémoire. Les pages utilisent le code de téléchargement standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.

Renvoie : __dlBegin(name, type, size) renvoie un jeton (ou "" quand le téléphone n'a pas de place) ; __dlChunk(token, base64) renvoie false si une tranche n'a pas pu être écrite ; __dlEnd(token, action) termine (save = Téléchargements, open = l'afficher, share, copy). Nécessite : rien.

Utilisez l'API publique :

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"

Exporter un dessin depuis un canvas par le même chemin :

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

Enregistrer une sauvegarde JSON que l'utilisateur peut conserver, avec un message à la fin :

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

Remarques : Implémenté dans le shell de l'application (ShellDownloads.kt).

__dlEnd pont#

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

Termine le téléchargement [token] et agit dessus : [action] « save » (Téléchargements, avec la barre « Téléchargé · Ouvrir » et la notification), « open » (visionneuse PDF ou application du téléphone pour ce type), « share » (feuille de partage) ou « copy » (image dans le presse-papiers).

Exemple

Documenté avec WebToApk.__dlBegin : l'exemple qui s'y trouve illustre aussi celui-ci.

Transport interne derrière <a download>, les téléchargements de Blob, window.open(blobUrl) et AppMint.saveBlob / AppMint.downloadFile : le fichier est transmis de la page à l'application par tranches, de sorte qu'un gros export n'est jamais une seule chaîne géante en mémoire. Les pages utilisent le code de téléchargement standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.

Renvoie : __dlBegin(name, type, size) renvoie un jeton (ou "" quand le téléphone n'a pas de place) ; __dlChunk(token, base64) renvoie false si une tranche n'a pas pu être écrite ; __dlEnd(token, action) termine (save = Téléchargements, open = l'afficher, share, copy). Nécessite : rien.

Utilisez l'API publique :

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"

Exporter un dessin depuis un canvas par le même chemin :

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

Enregistrer une sauvegarde JSON que l'utilisateur peut conserver, avec un message à la fin :

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

Remarques : Implémenté dans le shell de l'application (ShellDownloads.kt).

__fsChild pont#

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

getFileHandle / getDirectoryHandle (avec `create`).

Exemple

Transport interne derrière getFileHandle(name, {create}) et getDirectoryHandle(name, {create}). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : un JSON {ok, kind, name, uri, canWrite} ou {ok:false, error} (NotFoundError, TypeMismatchError, NoModificationAllowedError). Nécessite : un dossier provenant de showDirectoryPicker().

Utilisez l'API publique :

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

Vérifier si un fichier existe déjà dans le dossier avant de le créer :

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

Enregistrer les paramètres de l'application en JSON dans un sous-dossier « config », en créant les deux si nécessaire :

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)

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsFile pont#

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

getFile() : `{ok, name, type, size, lastModified, url}` (le contenu est diffusé depuis l'url).

Exemple

Transport interne derrière FileSystemFileHandle.getFile(). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : un JSON {ok, name, type, size, lastModified, url} - le contenu est diffusé depuis url (l'origine propre de l'application, avec plages) - ou {ok:false, error} (NotFoundError, TypeMismatchError, foreign_origin). Nécessite : un handle issu de l'un des sélecteurs.

Utilisez l'API publique :

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

Afficher une image choisie sur la page sans la lire dans une chaîne :

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

Relire un fichier ouvert plus tôt et remarquer quand il a changé ou a été supprimé :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsList pont#

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

Les entrées d'un handle de dossier : `{ok, entries:[{kind, name, uri, canWrite}]}`.

Exemple

Transport interne derrière l'itération sur un FileSystemDirectoryHandle (entries(), keys(), values(), for await). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : un JSON {ok, entries:[{kind, name, uri, canWrite}]} ou {ok:false, error}. Nécessite : un dossier provenant de showDirectoryPicker().

Utilisez l'API publique :

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

Compter les fichiers d'un dossier et de tous ses sous-dossiers :

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

Construire une liste triée des photos d'un dossier choisi par l'utilisateur :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsPermission pont#

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

queryPermission() / requestPermission() : `{ok, state:'granted'|'prompt'}` pour un handle dont la racine choisie est [rootUri] - « granted » tant que l'application détient l'autorisation de ce choix, entre les relances (l'autorisation est conservée ; un handle revient depuis IndexedDB).

Exemple

Transport interne derrière FileSystemHandle.queryPermission() et requestPermission(). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : un JSON {ok:true, state:'granted'} tant que l'application détient l'autorisation du dossier ou du fichier choisi dont provient le handle (conservée entre les redémarrages), {ok:true, state:'prompt'} une fois qu'elle a disparu, ou {ok:false, error:'foreign_origin'}. Un fichier en lecture seule répond 'denied' pour {mode:'readwrite'} dans la page, sans appel. Nécessite : un handle issu de l'un des sélecteurs, y compris un handle relu depuis IndexedDB.

Utilisez l'API publique :

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

Redemandez l'accès depuis un appui quand l'autorisation a disparu - le sélecteur de dossier s'ouvre sur ce dossier, et seul ce même dossier accorde l'accès :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt). Voir showOpenFilePicker pour l'API File System Access complète.

__fsPick pont#

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

Ouvre le sélecteur [kind] (open | save | directory) ; les handles arrivent sous la forme `appmint:fs`.

Exemple

Transport interne derrière showOpenFilePicker(), showSaveFilePicker() et showDirectoryPicker(). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : {ok:true} une fois le sélecteur affiché, ou {ok:false, error} (NotAllowedError quand un sélecteur est déjà ouvert, foreign_origin). Les handles arrivent avec l'événement appmint:fs {requestId, handles:[{kind, name, uri, canWrite}]} ou {requestId, error:'cancelled'}. Nécessite : rien à activer.

Utilisez l'API publique :

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

Enregistrer un rapport avec un nom suggéré, depuis un appui sur un bouton (les sélecteurs nécessitent un geste de l'utilisateur) :

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

Laisser l'utilisateur choisir plusieurs photos à la fois, en partant du dossier Images :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt). Le même code de page fonctionne sans modification dans Chrome sur ordinateur.

__fsRemove pont#

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

removeEntry(name, {recursive}).

Exemple

Transport interne derrière FileSystemDirectoryHandle.removeEntry(name, {recursive}). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : un JSON {ok:true} ou {ok:false, error} (NotFoundError, InvalidModificationError pour un dossier non vide sans recursive). Nécessite : un dossier provenant de showDirectoryPicker().

Utilisez l'API publique :

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

Supprimer un fichier et considérer « déjà supprimé » comme terminé :

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

Supprimer un dossier, en ne demandant d'abord confirmation que s'il contient encore quelque chose :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsWritableAbort pont#

window.WebToApk.__fsWritableAbort(token: String)

abort() : le remplacement est abandonné ; le document n'est pas modifié.

Exemple

Transport interne derrière FileSystemWritableFileStream.abort() - la copie d'échange est abandonnée et le fichier reste tel quel. Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : rien. Nécessite : un flux d'écriture ouvert.

Utilisez l'API publique :

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

Laisser l'utilisateur annuler un long export ; rien d'à moitié écrit n'arrive dans le fichier :

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

Télécharger directement dans un fichier avec pipeTo ; arrêter le téléchargement annule l'écriture :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsWritableClose pont#

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

close() : la copie d'échange remplace le document.

Exemple

Transport interne derrière FileSystemWritableFileStream.close() - la copie d'échange remplace le fichier. Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Retour : JSON {ok:true} ou {ok:false, error} (NoModificationAllowedError, InvalidModificationError quand l'application du fichier ne peut pas le raccourcir). Nécessite : un flux en écriture ouvert.

Utilisez l'API publique :

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

Dire à l'utilisateur pourquoi un enregistrement n'a pas abouti (close() est l'endroit où il peut échouer) :

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

Écrire un fichier en plusieurs morceaux ; les lecteurs voient toujours l'ancien contenu jusqu'à ce que l'unique 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');
}

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsWritableOpen pont#

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

createWritable() : un fichier d'échange, copié depuis le fichier quand [keepExisting].

Exemple

Transport interne derrière FileSystemFileHandle.createWritable({keepExistingData}). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Renvoie : un JSON {ok, token} pour une copie d'échange privée, ou {ok:false, error} (NoModificationAllowedError pour un fichier en lecture seule). Nécessite : un handle issu de l'un des sélecteurs.

Utilisez l'API publique :

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

Ajouter une ligne à la fin d'un fichier journal existant, en conservant son contenu :

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
}

Enregistrer un canvas en PNG, et laisser le fichier intact si quelque chose se passe mal :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsWritableTruncate pont#

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

truncate(size) sur un flux d'écriture.

Exemple

Transport interne derrière FileSystemWritableFileStream.truncate(size). Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Retour : JSON {ok:true} ou {ok:false, error}. Nécessite : un flux en écriture ouvert.

Utilisez l'API publique :

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

Ne garder que les 1 000 premiers octets d'un fichier et couper tout ce qui suit :

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

Réserver de l'espace : tronquer à une taille plus grande agrandit le fichier, et les nouveaux octets sont relus comme des zéros :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt).

__fsWritableWrite pont#

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

Un bloc du write() d'un flux d'écriture, à la [position].

Exemple

Transport interne derrière FileSystemWritableFileStream.write() - un bloc à une position dans la copie d'échange. Les pages utilisent l'API standard ; n'appelez jamais ceci directement - les méthodes __ peuvent changer sans préavis.

Retour : JSON {ok:true} ou {ok:false, error}. Nécessite : un flux en écriture ouvert.

Utilisez l'API publique :

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

Ajouter une ligne à la fin d'un fichier journal choisi par l'utilisateur (un sélecteur de fichier nécessite un appui) :

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

Enregistrer un dessin de canvas en nouveau PNG avec « Enregistrer sous » (un Blob est écrit tel quel, en octets bruts) :

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

Remarques : Implémenté dans le shell de l'application (FileSystemAccess.kt). Les grosses données sont envoyées par blocs.

getMaxIoChunkSize pont#

window.WebToApk.getMaxIoChunkSize(): Long

La limite d'octets par appel pour [readFileBase64] / [writeFileBase64], pour calculer les blocs.

Exemple

La limite d'octets par appel de readFileBase64 / writeFileBase64 (4 Mo). Les pages utilisent normalement WebToApkFS.maxChunkSize().

Renvoie : (sync) un nombre (4194304). Nécessite : rien pour l'appeler ; les méthodes de lecture/écriture qu'il décrit nécessitent que Native Folder Access (SAF) soit coché à l'étape 4 (Accès).

La méthode publique :

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

L'appel brut :

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

Calculer combien d'appels au pont un fichier nécessitera, pour dimensionner une barre de progression avant de le copier :

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

Copier un fichier dans un autre, un bloc de taille maximale à la fois :

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)

Remarques : Une écriture plus grande que cette limite est refusée avec chunk too large (max N bytes) ; une lecture plus grande est tronquée à cette taille.

pickFile pont#

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

Ouvre le sélecteur système à fichier unique (ACTION_OPEN_DOCUMENT). Contrairement à [requestFolderAccess], il accorde l'accès à exactement un fichier, ce que doit demander un éditeur ou un destructeur de fichiers ; l'autorisation de lecture/écriture est conservée, de sorte que l'URI continue de fonctionner après l'arrêt du processus. La réponse arrive sur `WebToApkOnFilePickResult(success, fileJson, requestId)` ; l'`error` d'un échec est « cancelled », « disabled », « no-picker », « foreign_origin » ou « busy » (un sélecteur est déjà ouvert pour une demande précédente, qui garde sa propre réponse). @param mimeFilter par ex. « image/\* », « application/pdf », ou "" pour tout fichier. (L'étoile est échappée par une barre oblique inverse uniquement parce que les commentaires de bloc Kotlin s'imbriquent.)

Exemple

Ouvre le sélecteur système pour UN fichier. Les pages utilisent normalement l'assistant Promise WebToApkFS.pickFile(mimeFilter) ; cette méthode brute répond par un callback.

Renvoie : rien pour l'instant. Plus tard, le shell appelle window.WebToApkOnFilePickResult(success, fileJson, requestId), où fileJson est une chaîne JSON {ok, uri, name, size, mime} en cas de succès ou {ok:false, uri:'', error} avec error = 'cancelled', 'disabled', 'no-picker', 'busy' ou 'foreign_origin'. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

La méthode publique (recommandée) :

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

L'appel brut, avec votre propre correspondance 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
  });
}

Joindre un PDF et afficher son nom et sa taille, en distinguant une annulation d'un vrai problème :

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

Ouvrir un fichier texte et charger son contenu dans un éditeur :

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
}

Remarques : Une seule sélection peut être ouverte à la fois : un appel fait pendant qu'une autre est ouverte reçoit aussitôt error:'busy', et celle qui est ouverte garde sa propre réponse. L'autorisation est conservée (lecture et écriture) jusqu'à ce que vous appeliez releaseFileAccess(uri). Le fichier n'est pas lu ici - utilisez readFileBase64 / WebToApkFS.readBytes avec le uri.

readFileBase64 pont#

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

Lit jusqu'à [length] octets bruts à partir de [offset] et les renvoie en base64. @param pathOrUri un URI `content://` provenant de [pickFile], ou un chemin relatif au dossier autorisé par [requestFolderAccess]. @param length octets à lire ; limité à 4 Mo par appel. 0 ou négatif signifie « autant qu'un appel le permet ». @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` indique à une boucle de lecture par blocs quand s'arrêter sans avoir à comparer elle-même avec `size`.

Exemple

Lit des octets bruts d'un fichier à un décalage donné et les renvoie en base64. Les pages utilisent normalement WebToApkFS.readBytes(pathOrUri, offset, length) (même résultat, déjà analysé) ou WebToApkFS.readAll(pathOrUri).

Renvoie : (sync) une chaîne JSON {ok:true, base64, bytesRead, offset, size, eof} ou {ok:false, error}. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

La méthode publique :

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

L'appel brut - notez que le résultat est une chaîne que vous devez passer à JSON.parse, pas du base64 brut :

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

Vérifier qu'un fichier choisi est bien un PNG en ne lisant que ses 8 premiers octets :

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

Lire un gros fichier bloc par bloc avec une barre de progression, sans jamais le garder entièrement en mémoire :

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
}

Remarques : pathOrUri est un URI content:// provenant de pickFile ou un chemin relatif au dossier obtenu avec requestFolderAccess. Au plus 4 Mo par appel (getMaxIoChunkSize()) ; bouclez sur bytesRead jusqu'à eof. Erreurs : not-found, disabled, cannot-open, offset must be >= 0, offset past end of file (N bytes), foreign_origin (une page d'un site autre que celui de l'application).

releaseFileAccess pont#

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

Libère une autorisation de fichier unique conservée, obtenue par [pickFile]. Android limite le nombre d'autorisations d'URI qu'une application peut conserver, donc une application qui choisit beaucoup de fichiers devrait libérer ceux dont elle a fini.

Exemple

Libère l'autorisation de lecture/écriture conservée pour un fichier choisi avec pickFile. Les pages utilisent normalement WebToApkFS.releaseFile(uri), qui renvoie un vrai booléen.

Renvoie : (sync) la chaîne "true" ou "false". Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation (toujours "false" sinon).

La méthode publique :

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

L'appel brut :

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

Garder une liste de « fichiers récents » de 10 au maximum et libérer l'autorisation de tout fichier qui en sort :

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

Fermer le document actuel : libérer son autorisation et oublier l'URI enregistré :

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

Remarques : Android limite le nombre d'autorisations de fichiers qu'une application peut conserver ; libérez donc les fichiers dont vous n'avez plus besoin. Le fichier lui-même n'est pas touché.

statFile pont#

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

Métadonnées d'un fichier ou d'un dossier sans en lire le contenu. Un chemin relatif désigne un fichier ou un sous-dossier du dossier autorisé ; un dossier répond `isDirectory: true` avec `size` 0 et `mime` `vnd.android.document/directory`. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, ou `{ok:false, error}` avec `not-found`, `disabled` ou `foreign_origin`.

Exemple

Lit les métadonnées d'un fichier (nom, taille, type) sans lire ses octets. Les pages utilisent normalement WebToApkFS.stat(pathOrUri), qui analyse le JSON pour vous.

Renvoie : (sync) une chaîne JSON {ok, name, size, mime, lastModified, uri, isDirectory, canWrite}, ou {ok:false, error} ('not-found', 'disabled', 'foreign_origin'). Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

La méthode publique :

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

L'appel brut :

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

Refuser un fichier trop gros avant d'en lire quoi que ce soit :

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

Chercher un sous-dossier du dossier autorisé et ne proposer « Enregistrer ici » que s'il est accessible en écriture :

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

Remarques : Accepte un URI content:// provenant de pickFile ou un chemin relatif au dossier autorisé avec requestFolderAccess. Il ne répond qu'aux pages de l'application elle-même ou à son site web configuré ; une page de n'importe quel autre site reçoit foreign_origin.

writeFileBase64 pont#

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

Écrit des octets bruts et confirme combien sont arrivés. @param mode `"patch"` écrit à [offset] en laissant le reste du fichier intact (ce que nécessite un écrasement en plusieurs passes), `"truncate"` remplace le fichier, `"append"` ignore [offset] et écrit à la fin. @return JSON `{ok, bytesWritten, offset, size, error}` - `size` est relue depuis le fichier après un fsync, de sorte qu'un appelant peut vraiment vérifier l'écriture plutôt que de faire confiance à un booléen.

Exemple

Écrit des octets base64 dans un fichier à un décalage (patch), sur tout le fichier (truncate) ou à la fin (append). Les pages utilisent normalement WebToApkFS.writeBytes(pathOrUri, base64, offset, mode) ou WebToApkFS.writeAll(pathOrUri, base64).

Renvoie : (sync) une chaîne JSON {ok, bytesWritten, offset, size} (size relue après un fsync) ou {ok:false, error}. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

La méthode publique :

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

L'appel brut :

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

Enregistrer un dessin de canvas en PNG dans un fichier choisi par l'utilisateur, en remplaçant son contenu :

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

Écraser un fichier sur place avec des zéros avant de le supprimer, et confirmer que chaque passe a bien été effectuée :

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
}

Remarques : Le mode est 'patch' (par défaut si vide), 'truncate' ou 'append'. Un appel accepte au plus 4 Mo d'octets décodés (getMaxIoChunkSize()). Un chemin relatif est créé dans le dossier autorisé s'il manque. Erreurs : disabled, foreign_origin (la page à l'écran n'appartient pas à l'application), bad mode: x, offset must be >= 0, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

Généré à partir de l'environnement d'exécution de l'application et de ses fichiers d'exemple à chaque compilation de la documentation. Lire en Markdown · Toutes les familles.

Vérifié par rapport au pont publié le 22/10/2026.