Créer un APK gratuit

API du pont JavaScript

Fichiers : un dossier autorisé par l'utilisateur

Un répertoire choisi une fois, lisible et modifiable ensuite, avec une autorisation qui survit aux redémarrages.

WebToApkFS.clearAccess assistant#

WebToApkFS.clearAccess()

Exemple

Oublie le dossier autorisé et rend l'autorisation à Android.

Retour : rien (synchrone). Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Proposez toujours un moyen de se déconnecter à côté du nom du dossier :

document.getElementById('disconnect').addEventListener('click', function () {
  if (!window.WebToApkFS) return;
  window.WebToApkFS.clearAccess();
  console.log('still connected?', window.WebToApkFS.hasAccess());   // false
  document.getElementById('folder-name').textContent = 'No folder chosen';
  document.getElementById('choose-folder').hidden = false;
});

Un bouton qui connecte ou déconnecte, selon l'état actuel :

var btn = document.getElementById('folder-toggle');
function paint() {
  btn.textContent = window.WebToApkFS.hasAccess() ? 'Disconnect folder' : 'Connect a folder';
}

btn.addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  if (window.WebToApkFS.hasAccess()) {
    if (!confirm('Stop saving to this folder? Your files stay where they are.')) return;
    window.WebToApkFS.clearAccess();
  } else {
    await window.WebToApkFS.requestAccess();
  }
  paint();
});
if (window.WebToApkFS) paint();

Changer de compte : oublier le dossier avec le reste de l'état de connexion :

function switchAccount() {
  sessionStorage.clear();
  if (window.WebToApkFS) window.WebToApkFS.clearAccess();   // the next account picks its own folder
  showLoginScreen();
}

Remarques : Les fichiers restent où ils sont ; l'application ne peut simplement plus y accéder. Pour réutiliser un dossier, appelez WebToApkFS.requestAccess() depuis un appui.

WebToApkFS.delete assistant#

WebToApkFS.delete(relativePath, options)

Exemple

Supprime un fichier ou un dossier dans le dossier autorisé.

Retour : un booléen, de façon synchrone : true quand l'élément a été supprimé. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation, et un dossier autorisé.

Supprimer un fichier, après confirmation :

if (window.WebToApkFS && confirm('Delete notes/old.txt?')) {
  var ok = window.WebToApkFS.delete('notes/old.txt');
  if (!ok) alert('Could not delete the file.');
}

Supprimer un dossier et tout son contenu - passez { recursive: true }:

if (window.WebToApkFS && confirm('Delete the whole 2025 folder?')) {
  var gone = window.WebToApkFS.delete('notes/2025', { recursive: true });
  console.log(gone ? 'deleted' : 'not deleted');
}

Nettoyer les exports de plus de 30 jours, fichier par fichier :

function pruneOldExports() {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return 0;
  var cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
  var removed = 0;
  window.WebToApkFS.list('exports').forEach(function (e) {
    if (e.isDirectory || e.lastModified >= cutoff) return;
    if (window.WebToApkFS.delete('exports/' + e.name)) removed++;
  });
  return removed;
}

if (confirm('Delete exports older than 30 days?')) {
  document.getElementById('status').textContent = pruneOldExports() + ' old exports deleted';
}

Remarques : Sans { recursive: true }, un dossier n'est supprimé que s'il est vide. Le dossier autorisé lui-même ('') ne peut jamais être supprimé. La suppression est immédiate et il n'y a pas de corbeille ; demandez donc toujours confirmation à l'utilisateur.

WebToApkFS.getAccessUri assistant#

WebToApkFS.getAccessUri()

Exemple

Donne l'adresse du dossier autorisé, pour pouvoir montrer à l'utilisateur où vont les fichiers.

Retour : une chaîne, de façon synchrone : un URI d'arborescence comme content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes, ou '' quand aucun dossier n'est autorisé. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Afficher un nom de dossier lisible à côté d'un bouton Déconnecter :

function connectedFolderName() {
  if (!window.WebToApkFS) return '';
  var uri = window.WebToApkFS.getAccessUri();
  if (!uri) return '';
  var tail = uri.split('/tree/')[1] || '';
  try { tail = decodeURIComponent(tail); } catch (e) {}
  return tail.split(':').pop() || tail;        // "Documents/Notes"
}

var name = connectedFolderName();
document.getElementById('folder-name').textContent = name ? 'Folder: ' + name : 'No folder chosen';

Remarquer que l'utilisateur a choisi un autre dossier depuis la dernière visite, et reconstruire ce qui était mis en cache sur l'ancien :

function checkFolderChanged() {
  if (!window.WebToApkFS) return;
  var uri = window.WebToApkFS.getAccessUri();
  var last = '';
  try { last = localStorage.getItem('folderUri') || ''; } catch (e) {}
  if (uri && uri !== last) {
    rebuildFileIndex();                         // your app: re-read the new folder
    try { localStorage.setItem('folderUri', uri); } catch (e) {}
  }
}

checkFolderChanged();

Un bouton Déconnecter qui oublie le dossier et confirme qu'il a disparu :

document.getElementById('disconnect').addEventListener('click', function () {
  if (!window.WebToApkFS || !window.WebToApkFS.getAccessUri()) return;
  if (!confirm('Stop saving to this folder? Your files stay where they are.')) return;
  window.WebToApkFS.clearAccess();
  var still = window.WebToApkFS.getAccessUri();
  document.getElementById('folder-name').textContent = still ? 'Could not disconnect' : 'No folder chosen';
});

Remarques : Les méthodes de fichiers ne prennent pas cet URI - elles prennent des chemins relatifs au dossier autorisé ('notes/today.txt'). Le format de l'URI dépend du fournisseur de fichiers choisi par l'utilisateur ; ne l'utilisez donc que pour l'affichage.

WebToApkFS.hasAccess assistant#

WebToApkFS.hasAccess()

Exemple

Indique si l'utilisateur a déjà donné un dossier à l'application.

Retour : un booléen, de façon synchrone. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Afficher « Choisir un dossier » ou la liste de fichiers, selon la réponse :

function renderFolderScreen() {
  if (!window.WebToApkFS) {
    document.getElementById('folder-panel').hidden = true;   // not in the app, or not enabled
    return;
  }
  var connected = window.WebToApkFS.hasAccess();
  document.getElementById('choose-folder').hidden = connected;
  document.getElementById('file-list').hidden = !connected;
  if (connected) {
    var entries = window.WebToApkFS.list('');
    document.getElementById('file-list').textContent =
      entries.map(function (e) { return e.name; }).join('\n');
  }
}

N'enregistrer automatiquement que lorsqu'un dossier est connecté, et le dire quand ce n'est pas le cas :

var saveTimer = null;
document.getElementById('editor').addEventListener('input', function () {
  clearTimeout(saveTimer);
  saveTimer = setTimeout(function () {
    var status = document.getElementById('status');
    if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) {
      status.textContent = 'Choose a folder to turn on autosave';
      return;
    }
    var ok = window.WebToApkFS.writeText('notes/draft.txt', document.getElementById('editor').value);
    status.textContent = ok ? 'Saved' : 'Could not save';
  }, 800);
});

Au démarrage, rouvrir la dernière note quand un dossier est mémorisé, sinon afficher l'écran d'accueil :

window.addEventListener('load', function () {
  var fs = window.WebToApkFS;
  if (!fs || !fs.hasAccess()) {
    showWelcome();                                 // your app: explains "Choose folder"
    return;
  }
  var last = 'notes/today.txt';
  try { last = localStorage.getItem('lastNote') || last; } catch (e) {}
  document.getElementById('editor').value = fs.readText(last);
});

Remarques : Elle indique seulement que l'application mémorise un dossier. Si l'utilisateur a supprimé ou déplacé ce dossier, list('') renvoie [] - proposez de nouveau « Choisir un dossier ». window.WebToApkFS apparaît à la fin du chargement de la page ; pendant le chargement, utilisez la méthode brute window.WebToApk.hasFolderAccess() === 'true'.

WebToApkFS.isAvailable assistant#

WebToApkFS.isAvailable()

Exemple

Indique si cette application peut utiliser un dossier autorisé par l'utilisateur.

Retour : un booléen, de façon synchrone : true quand la compilation a l'accès aux dossiers activé. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation - sans cela (et sans Clés sécurisées), window.WebToApkFS n'est pas ajouté à la page ; avec seulement les Clés sécurisées, il existe et répond false.

window.WebToApkFS est ajouté avant l'exécution du premier script de votre page ; sur les WebViews antérieurs à 2021 environ, il n'arrive qu'à la fin du chargement de la page. Pour couvrir aussi ceux-là, attendez-le brièvement avant de décider :

function whenFolderHelperReady(done) {
  var tries = 0;
  (function check() {
    if (window.WebToApkFS) return done(window.WebToApkFS);
    if (++tries > 30) return done(null);       // in a browser, or the build did not enable it
    setTimeout(check, 100);
  })();
}

whenFolderHelperReady(function (fs) {
  var usable = !!(fs && fs.isAvailable());
  document.getElementById('folder-panel').hidden = !usable;
});

Dans un appui sur un bouton, la page a fini de charger depuis longtemps ; une vérification directe suffit donc :

if (window.WebToApkFS && window.WebToApkFS.isAvailable()) {
  // show the folder features
}

Transformer les deux vérifications en un seul état qui pilote tout l'écran du dossier :

function folderState() {
  var fs = window.WebToApkFS;
  if (!fs || !fs.isAvailable()) return 'unsupported';   // this build has no folder access
  return fs.hasAccess() ? 'ready' : 'needs-folder';
}

var state = folderState();
document.getElementById('folder-panel').hidden = state === 'unsupported';
document.getElementById('choose-folder').hidden = state !== 'needs-folder';
document.getElementById('export').disabled = state !== 'ready';

Remarques : Cela ne dit pas si un dossier est déjà choisi - utilisez WebToApkFS.hasAccess() pour cela. Avant le chargement de la page, la méthode brute window.WebToApk.isFolderAccessEnabled() (chaîne "true"/"false") répond à la même question.

WebToApkFS.list assistant#

WebToApkFS.list(relativePath)

Exemple

Liste les fichiers et sous-dossiers d'un dossier du dossier autorisé.

Retour : un tableau, de façon synchrone : [{ name, uri, isDirectory, length, lastModified }] (length en octets, lastModified en ms depuis 1970). Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation, et un dossier autorisé.

Un explorateur de fichiers simple ('' est le dossier autorisé lui-même) :

function renderFolder(path) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return;
  var ul = document.getElementById('files');
  ul.innerHTML = '';
  window.WebToApkFS.list(path)
    .sort(function (a, b) { return (b.isDirectory - a.isDirectory) || a.name.localeCompare(b.name); })
    .forEach(function (e) {
      var li = document.createElement('li');
      li.textContent = (e.isDirectory ? '[folder] ' : '') + e.name +
        (e.isDirectory ? '' : ' (' + e.length + ' bytes, ' + new Date(e.lastModified).toLocaleDateString() + ')');
      if (e.isDirectory) li.onclick = function () { renderFolder(path ? path + '/' + e.name : e.name); };
      ul.appendChild(li);
    });
}

renderFolder('');

Parcourir chaque sous-dossier pour collecter toutes les notes Markdown (chemins que vous pouvez passer à readText):

function findNotes(path, out) {
  out = out || [];
  window.WebToApkFS.list(path).forEach(function (e) {
    var full = path ? path + '/' + e.name : e.name;
    if (e.isDirectory) findNotes(full, out);
    else if (/\.md$/i.test(e.name)) out.push(full);
  });
  return out;
}

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  var notes = findNotes('');
  document.getElementById('count').textContent = notes.length + ' notes';
}

Trouver la sauvegarde la plus récente et l'espace qu'occupent les sauvegardes :

function backupSummary() {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return null;
  var files = window.WebToApkFS.list('backups').filter(function (e) { return !e.isDirectory; });
  if (!files.length) return null;
  var newest = files.reduce(function (a, b) { return b.lastModified > a.lastModified ? b : a; });
  var bytes = files.reduce(function (sum, e) { return sum + e.length; }, 0);
  return { newest: 'backups/' + newest.name, count: files.length, kb: Math.round(bytes / 1024) };
}

var s = backupSummary();
document.getElementById('backup-info').textContent = s
  ? s.count + ' backups, ' + s.kb + ' KB. Latest: ' + s.newest
  : 'No backups yet';

Remarques : Un dossier vide, un dossier manquant, un chemin qui est un fichier, un chemin avec .., et l'absence de dossier autorisé renvoient tous []. La liste n'est pas triée. Chaque uri fonctionne aussi avec WebToApkFS.stat() et WebToApkFS.readBytes().

WebToApkFS.mkdir assistant#

WebToApkFS.mkdir(relativePath)

Exemple

Crée un dossier dans le dossier autorisé, y compris les dossiers parents manquants.

Retour : un booléen, de façon synchrone : true quand le dossier existe ensuite. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation, et un dossier autorisé.

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  var month = new Date().toISOString().slice(0, 7);          // "2026-06"
  if (window.WebToApkFS.mkdir('journal/' + month)) {
    window.WebToApkFS.writeText('journal/' + month + '/readme.txt', 'One file per day.');
  } else {
    alert('Could not create the folder.');
  }
}

Préparer l'arborescence de dossiers d'un projet à la première utilisation, et signaler tout dossier qui n'a pas pu être créé :

function createProjectFolders(project) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
  var failed = ['images', 'exports', 'drafts'].filter(function (sub) {
    return !window.WebToApkFS.mkdir('projects/' + project + '/' + sub);
  });
  if (failed.length) alert('Could not create: ' + failed.join(', '));
  return failed.length === 0;
}

createProjectFolders('holiday-2026');

Un bouton « Nouveau dossier » qui avertit quand le nom est déjà pris (car mkdir répond true pour un dossier existant) :

document.getElementById('new-folder').addEventListener('click', function () {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return;
  var name = (prompt('Folder name') || '').trim();
  if (!name || /[\\/]/.test(name) || name === '.' || name === '..') return;
  var taken = window.WebToApkFS.list('').some(function (e) { return e.name === name; });
  if (taken) { alert('"' + name + '" already exists.'); return; }
  if (window.WebToApkFS.mkdir(name)) renderFolder('');   // your app: refresh the list
  else alert('Could not create "' + name + '".');
});

Remarques : Elle renvoie true si le dossier existe déjà, on peut donc l'appeler avant chaque enregistrement. Elle renvoie false quand un FICHIER du même nom gêne, que le chemin contient .., ou qu'aucun dossier n'est autorisé. WebToApkFS.writeText() crée déjà les parents manquants ; vous n'avez donc besoin de mkdir que pour les dossiers vides.

WebToApkFS.readText assistant#

WebToApkFS.readText(relativePath)

Exemple

Lit un fichier texte du dossier autorisé en UTF-8.

Retour : une chaîne, de façon synchrone : le texte du fichier, ou ''. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation, et un dossier autorisé.

'' signifie « fichier vide » OU « fichier inexistant ». Utilisez WebToApkFS.stat() quand vous devez savoir lequel :

function loadSettings() {
  if (!window.WebToApkFS) return null;
  var info = window.WebToApkFS.stat('data/settings.json');
  if (!info.ok) return null;                                // not saved yet
  try {
    return JSON.parse(window.WebToApkFS.readText('data/settings.json'));
  } catch (e) {
    alert('settings.json is damaged: ' + e.message);
    return null;
  }
}

Ouvrir une note dans une zone de texte :

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  document.getElementById('editor').value = window.WebToApkFS.readText('notes/today.txt');
}

Importer un CSV que l'utilisateur a déposé dans le dossier :

function importCsv(path) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return [];
  var text = window.WebToApkFS.readText(path);
  if (!text) return [];                                     // missing or empty
  var lines = text.replace(/\r/g, '').split('\n').filter(Boolean);
  var header = lines.shift().split(',');
  return lines.map(function (line) {
    var cells = line.split(',');
    var row = {};
    header.forEach(function (h, i) { row[h.trim()] = (cells[i] || '').trim(); });
    return row;
  });
}

var contacts = importCsv('import/contacts.csv');
console.log(contacts.length + ' rows imported');

Remarques : Le fichier entier est chargé d'un coup - pour les gros fichiers ou les fichiers binaires, utilisez WebToApkFS.readBytes(). Les octets qui ne sont pas de l'UTF-8 valide reviennent sous forme de caractère de remplacement. Un dossier, un chemin avec .., ou l'absence de dossier autorisé renvoie aussi ''.

WebToApkFS.rename assistant#

WebToApkFS.rename(relativePath, newName)

Exemple

Renomme un fichier ou un dossier dans le dossier autorisé.

Retour : un booléen, de façon synchrone. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation, et un dossier autorisé.

newName est seulement le nouveau nom, pas un chemin. L'entrée reste dans le même dossier :

function renameNote(path, newName) {
  if (!window.WebToApkFS) return false;
  var ok = window.WebToApkFS.rename(path, newName);     // 'notes/a.txt', 'b.txt' -> notes/b.txt
  if (!ok) alert('Could not rename. A file called "' + newName + '" may already exist.');
  return ok;
}

renameNote('notes/today.txt', 'journal.txt');
renameNote('notes/2025', 'archive-2025');               // folders work too

Renommer sans conflit : essayer « nom (2) », « nom (3) » … jusqu'à trouver un nom libre :

function renameUnique(path, wanted) {
  if (!window.WebToApkFS) return '';
  var dot = wanted.lastIndexOf('.');
  var base = dot > 0 ? wanted.slice(0, dot) : wanted;
  var ext = dot > 0 ? wanted.slice(dot) : '';
  for (var n = 1; n <= 20; n++) {
    var name = n === 1 ? wanted : base + ' (' + n + ')' + ext;
    if (window.WebToApkFS.rename(path, name)) return name;
  }
  return '';
}

var finalName = renameUnique('photos/IMG_0042.jpg', 'Beach.jpg');

Enregistrer en toute sécurité : écrire d'abord un fichier temporaire, puis le mettre en place (le renommage refuse un nom déjà pris, l'ancien fichier est donc supprimé juste avant) :

function saveSafely(folder, name, text) {
  var fs = window.WebToApkFS;
  if (!fs || !fs.hasAccess()) return false;
  var tmp = folder + '/' + name + '.tmp';
  if (!fs.writeText(tmp, text)) return false;           // the original is untouched
  fs.delete(folder + '/' + name);                       // false when it did not exist yet
  return fs.rename(tmp, name);
}

saveSafely('notes', 'today.txt', document.getElementById('editor').value);

Remarques : Elle renvoie false quand une autre entrée porte déjà newName, quand newName est vide, ./.., ou contient / ou \, quand le chemin n'existe pas, ou qu'aucun dossier n'est autorisé. Pour déplacer un fichier entre dossiers, lisez-le, écrivez-le au nouvel endroit, puis supprimez l'ancien.

WebToApkFS.requestAccess assistant#

WebToApkFS.requestAccess()

Exemple

Ouvre le sélecteur de dossier du système pour que l'utilisateur donne un dossier à l'application, conservé entre les redémarrages.

Retour : une Promise qui se résout toujours (ne rejette jamais) avec { success, folderUri, requestId }, plus error quand success est faux. success est un booléen ; folderUri est l'adresse content://…/tree/… du dossier autorisé, ou ''. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Demander depuis un bouton sur lequel l'utilisateur appuie, et afficher quel dossier est connecté :

document.getElementById('choose-folder').addEventListener('click', async function () {
  if (!window.WebToApkFS) {
    alert('Folder access is not available in this app.');
    return;
  }
  var r = await window.WebToApkFS.requestAccess();
  if (!r.success) {
    document.getElementById('status').textContent = 'No folder chosen.';
    return;
  }
  var name = decodeURIComponent(r.folderUri.split('/tree/')[1] || '').split(':').pop();
  document.getElementById('status').textContent = 'Saving to: ' + name;
  window.WebToApkFS.writeText('hello.txt', 'Connected at ' + new Date().toISOString());
});

Ne demander que si aucun dossier n'est encore autorisé :

async function ensureFolder() {
  if (!window.WebToApkFS) return false;
  if (window.WebToApkFS.hasAccess()) return true;
  var r = await window.WebToApkFS.requestAccess();
  return r.success;
}

Un bouton « Changer de dossier » qui explique chaque cause d'échec de la demande :

var folderErrors = {
  cancelled: 'No folder chosen.',
  busy: 'The folder picker is already open.',
  not_persisted: 'Android would not keep access to that folder. Try another one.',
  disabled: 'Folder access is switched off in this app.',
  no_picker: 'This phone has no folder picker.',
  foreign_origin: 'This page is not allowed to use folders.'
};

document.getElementById('change-folder').addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  var r = await window.WebToApkFS.requestAccess();
  document.getElementById('status').textContent = r.success
    ? 'Folder changed.'
    : (folderErrors[r.error] || 'Could not open the folder picker.');
  if (r.success) renderFolder('');                      // your app: show the new folder
});

Remarques : Appelez-la depuis un appui, jamais au chargement de la page - le sélecteur recouvre l'application. success: false vient avec error : 'cancelled' (l'utilisateur a annulé), 'not_persisted' (Android n'a pas conservé l'autorisation), 'disabled', 'busy' (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), 'no_picker' ou 'foreign_origin'. Choisir de nouveau remplace le dossier précédent.

WebToApkFS.writeText assistant#

WebToApkFS.writeText(relativePath, content)

Exemple

Écrit un fichier texte UTF-8 dans le dossier autorisé, en le créant (ainsi que les dossiers parents manquants) ou en le remplaçant.

Retour : un booléen, de façon synchrone : true quand le fichier a été écrit. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation, et un dossier autorisé.

Enregistrer depuis un bouton :

document.getElementById('save').addEventListener('click', async function () {
  if (!window.WebToApkFS) { alert('Saving to a folder is not available here.'); return; }
  if (!window.WebToApkFS.hasAccess()) {
    var r = await window.WebToApkFS.requestAccess();
    if (!r.success) return;                              // user cancelled
  }
  var ok = window.WebToApkFS.writeText('notes/today.txt', document.getElementById('editor').value);
  document.getElementById('status').textContent = ok ? 'Saved' : 'Could not save';
});

Enregistrer un objet au format JSON :

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  window.WebToApkFS.writeText('data/settings.json', JSON.stringify({ theme: 'dark', size: 16 }, null, 2));
}

Ajouter une ligne à un journal quotidien (il n'y a pas d'ajout : lisez, ajoutez, puis réécrivez) :

function logLine(message) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
  var path = 'logs/' + new Date().toISOString().slice(0, 10) + '.log';   // one file per day
  var old = window.WebToApkFS.readText(path);
  var line = new Date().toLocaleTimeString() + '  ' + message + '\n';
  return window.WebToApkFS.writeText(path, old + line);
}

logLine('Order #1042 marked as shipped');

Remarques : Un fichier existant est entièrement remplacé - il n'y a pas d'ajout. content est converti en chaîne (null devient ''). Elle renvoie false quand le chemin est un dossier existant, contient .., ou qu'aucun dossier n'est autorisé. Le type de fichier vient de l'extension, donc le nom sur le disque est exactement le nom que vous avez donné.

clearFolderAccess pont#

window.WebToApk.clearFolderAccess()

Libère l'autorisation de dossier conservée. Refusé (sans effet) pour une page externe.

Exemple

Oublie le dossier autorisé et rend l'autorisation à Android.

Retour : rien (synchrone). WebToApkFS.clearAccess() fait la même chose. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) ; avec l'option désactivée, elle ne fait rien.

Un bouton « Déconnecter » :

document.getElementById('disconnect').addEventListener('click', function () {
  if (window.WebToApkFS) {
    window.WebToApkFS.clearAccess();
  } else if (window.WebToApk && typeof window.WebToApk.clearFolderAccess === 'function') {
    window.WebToApk.clearFolderAccess();
  }
  document.getElementById('folder-name').textContent = 'No folder chosen';
});

À la déconnexion, oublier aussi le dossier, pour que la personne suivante sur ce téléphone choisisse le sien :

function signOut() {
  localStorage.clear();
  if (window.WebToApk && typeof window.WebToApk.clearFolderAccess === 'function') {
    window.WebToApk.clearFolderAccess();
  }
  location.href = 'login.html';
}

Oublier un dossier déplacé ou supprimé dans l'application Fichiers, puis proposer d'en choisir un autre (l'application écrit myapp-folder.json quand un dossier est connecté pour la première fois) :

async function reconnectIfMissing() {
  var fs = window.WebToApkFS;
  if (!fs || !fs.hasAccess()) return;
  if (fs.stat('myapp-folder.json').ok) return;          // marker still there: the folder is fine
  if (typeof window.WebToApk.clearFolderAccess === 'function') {
    window.WebToApk.clearFolderAccess();                // drop the grant that points nowhere
  }
  if (confirm('The folder you chose earlier is gone. Choose a folder again?')) {
    var r = await fs.requestAccess();
    if (r.success) fs.writeText('myapp-folder.json', '{}');
  }
}

Remarques : Les fichiers du dossier ne sont PAS supprimés ; l'application ne peut simplement plus y accéder. Ensuite, hasFolderAccess() répond "false" et toutes les méthodes de fichiers échouent jusqu'à ce que l'utilisateur choisisse de nouveau un dossier.

deleteEntry pont#

window.WebToApk.deleteEntry(relativePath: String, recursive: String): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Supprime un fichier ou un dossier dans le dossier autorisé.

Retour : la CHAÎNE "true" ou "false", de façon synchrone. WebToApkFS.delete(path, { recursive }) renvoie un vrai booléen. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) et un dossier autorisé.

Avec l'assistant, après avoir demandé à l'utilisateur :

function removeEntry(path, isFolder) {
  if (!window.WebToApkFS) return false;
  if (!confirm('Delete ' + path + '? This cannot be undone.')) return false;
  return window.WebToApkFS.delete(path, { recursive: isFolder });
}

removeEntry('notes/old.txt', false);
removeEntry('notes/2025', true);      // the folder and everything in it

L'appel brut - le deuxième argument est la CHAÎNE 'true' ou 'false':

if (window.WebToApk && typeof window.WebToApk.deleteEntry === 'function') {
  var gone = window.WebToApk.deleteEntry('cache', 'true') === 'true';
}

Ne garder que les cinq sauvegardes les plus récentes et supprimer les autres :

function pruneBackups(keep) {
  var fs = window.WebToApkFS;
  if (!fs) return 0;
  var files = fs.list('backups').filter(function (e) { return !e.isDirectory; });
  files.sort(function (a, b) { return b.lastModified - a.lastModified; });   // newest first
  var removed = 0;
  files.slice(keep).forEach(function (e) {
    if (fs.delete('backups/' + e.name)) removed++;
  });
  return removed;
}

console.log(pruneBackups(5) + ' old backups deleted');

Remarques : Un dossier non vide n'est supprimé qu'avec 'true' récursif. Le dossier autorisé lui-même ('') ne peut jamais être supprimé. La suppression est réelle et immédiate : il n'y a pas de corbeille.

getFolderAccessUri pont#

window.WebToApk.getFolderAccessUri(): String

L'URI d'arborescence `content://` du dossier autorisé, ou "" (aucun, désactivé, ou page externe).

Exemple

Donne l'adresse du dossier que l'utilisateur a autorisé, pour pouvoir montrer quel dossier est connecté.

Retour : une chaîne, de façon synchrone : un URI d'arborescence comme content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes, ou "" quand aucun dossier n'est autorisé ou que l'option est désactivée. WebToApkFS.getAccessUri() renvoie la même chaîne. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès).

Transformer l'URI en un nom que l'utilisateur reconnaît :

function folderLabel() {
  var uri = '';
  if (window.WebToApkFS) {
    uri = window.WebToApkFS.getAccessUri();
  } else if (window.WebToApk && typeof window.WebToApk.getFolderAccessUri === 'function') {
    uri = window.WebToApk.getFolderAccessUri();
  }
  if (!uri) return '';
  var tail = uri.split('/tree/')[1] || '';
  try { tail = decodeURIComponent(tail); } catch (e) {}
  return tail.split(':').pop() || tail;          // "primary:Documents/Notes" -> "Documents/Notes"
}

var label = folderLabel();
document.getElementById('folder-name').textContent = label ? 'Saving to: ' + label : 'No folder chosen';

Recharger la liste de fichiers seulement quand l'utilisateur a vraiment choisi un autre dossier :

document.getElementById('change-folder').addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  var before = window.WebToApkFS.getAccessUri();
  var r = await window.WebToApkFS.requestAccess();
  if (!r.success) return;                        // the old folder is still connected
  if (r.folderUri !== before) {
    renderFiles(window.WebToApkFS.list(''));     // a new place: show what is in it
  }
});

Indiquer si le dossier est sur le téléphone, sur une carte SD ou dans une autre application de stockage :

function folderLocation() {
  var uri = window.WebToApkFS ? window.WebToApkFS.getAccessUri() : '';
  if (!uri) return 'none';
  var id = uri.split('/tree/')[1] || '';
  try { id = decodeURIComponent(id); } catch (e) {}
  if (uri.indexOf('com.android.externalstorage.documents') === -1) return 'other';   // e.g. a cloud drive app
  return id.indexOf('primary:') === 0 ? 'phone' : 'sd-card';                         // "1A2B-3C4D:Music" is a card
}

document.getElementById('folder-where').textContent =
  { none: '', phone: 'Phone storage', 'sd-card': 'SD card', other: 'Another storage app' }[folderLocation()];

Remarques : Vous ne repassez jamais cet URI aux méthodes de fichiers - elles prennent toutes des chemins relatifs à ce dossier. Affichez le nom à côté d'un bouton « Déconnecter » pour que l'utilisateur sache où vont les fichiers.

hasFolderAccess pont#

window.WebToApk.hasFolderAccess(): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Indique si l'utilisateur a déjà donné un dossier à l'application.

Retour : la CHAÎNE "true" ou "false" (pas un booléen), de façon synchrone. WebToApkFS.hasAccess() donne un vrai booléen. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) ; sans cela la réponse est toujours "false".

La méthode brute existe dès la toute première ligne de la page ; c'est donc la bonne vérification pendant que la page charge encore (WebToApkFS n'est ajouté qu'à la fin du chargement) :

function folderConnected() {
  if (window.WebToApkFS) return window.WebToApkFS.hasAccess();
  return !!(window.WebToApk && typeof window.WebToApk.hasFolderAccess === 'function' &&
    window.WebToApk.hasFolderAccess() === 'true');
}

document.getElementById('choose-folder').hidden = folderConnected();

Vérifier avant un enregistrement et n'ouvrir le sélecteur que si aucun dossier n'est encore connecté :

async function saveBackup(json) {
  if (!window.WebToApkFS) return false;
  if (!window.WebToApkFS.hasAccess()) {
    var r = await window.WebToApkFS.requestAccess();   // asked once; the grant is remembered
    if (!r.success) return false;
  }
  return window.WebToApkFS.writeText('backup.json', json);
}

document.getElementById('backup').addEventListener('click', function () {
  saveBackup(JSON.stringify(appState));
});

Un minuteur d'enregistrement automatique qui n'écrit que lorsqu'un dossier est connecté et n'ouvre jamais le sélecteur lui-même :

setInterval(function () {
  if (!window.WebToApk || typeof window.WebToApk.hasFolderAccess !== 'function') return;
  if (window.WebToApk.hasFolderAccess() !== 'true') return;   // not connected: the Save button asks
  window.WebToApk.writeTextFile('drafts/autosave.txt', document.getElementById('editor').value);
}, 30000);

Remarques : Comparez avec === 'true' ; la chaîne "false" est truthy. Elle indique seulement que l'application mémorise un dossier. Si ce dossier a été supprimé ou déplacé dans l'application Fichiers, listFolderEntries('') répond [] - demandez à l'utilisateur de le choisir de nouveau.

isFolderAccessEnabled pont#

window.WebToApk.isFolderAccessEnabled(): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Indique si cette application a été compilée avec l'accès aux dossiers activé.

Retour : la CHAÎNE "true" ou "false", de façon synchrone. Nécessite : rien pour l'appeler ; elle ne répond "true" que si Native Folder Access (SAF) a été coché à l'étape 4 (Accès).

L'utiliser pour décider d'afficher ou non la fonction de dossier. WebToApkFS.isAvailable() répond à la même question sous forme de booléen une fois la page chargée :

function folderFeatureShipped() {
  if (window.WebToApkFS) return window.WebToApkFS.isAvailable();
  return !!(window.WebToApk && typeof window.WebToApk.isFolderAccessEnabled === 'function' &&
    window.WebToApk.isFolderAccessEnabled() === 'true');
}

if (!folderFeatureShipped()) {
  document.getElementById('folder-panel').hidden = true;   // in a browser, or the switch was off
}

Piloter une ligne de paramètres selon ses trois états (absent de cette compilation, non connecté, connecté) :

function folderSettingsState() {
  var fs = window.WebToApkFS;
  if (!fs || !fs.isAvailable()) return 'unavailable';
  return fs.hasAccess() ? 'connected' : 'not-connected';
}

var row = document.getElementById('folder-row');
switch (folderSettingsState()) {
  case 'unavailable':   row.hidden = true; break;
  case 'not-connected': row.querySelector('button').textContent = 'Choose a folder'; break;
  case 'connected':     row.querySelector('button').textContent = 'Disconnect'; break;
}

Marquer la page avant son affichage, pour que le CSS puisse montrer les contrôles réservés aux dossiers sans clignotement :

// In a <script> inside <head>: the raw method is already there.
if (window.WebToApk && typeof window.WebToApk.isFolderAccessEnabled === 'function' &&
    window.WebToApk.isFolderAccessEnabled() === 'true') {
  document.documentElement.classList.add('has-folders');   // .has-folders .folder-only { display: block }
}

Remarques : Cela ne dit pas si un dossier est déjà choisi - utilisez hasFolderAccess() pour cela. Dans un navigateur, window.WebToApk n'existe pas, donc la vérification ci-dessus répond false sans lever d'erreur.

listFolderEntries pont#

window.WebToApk.listFolderEntries(relativePath: String): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Liste les fichiers et sous-dossiers d'un dossier du dossier autorisé.

Retour : une CHAÎNE JSON, de façon synchrone : un tableau de { name, uri, isDirectory, length, lastModified } (length en octets, lastModified en ms depuis 1970). WebToApkFS.list(path) l'analyse pour vous. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) et un dossier autorisé.

Avec l'assistant ('' est le dossier autorisé lui-même) :

function showFolder(path) {
  if (!window.WebToApkFS) return;
  var entries = window.WebToApkFS.list(path);          // already an array
  entries.forEach(function (e) {
    console.log(e.isDirectory ? '[dir] ' : '      ', e.name, e.length + ' bytes');
  });
}
showFolder('');          // the root
showFolder('notes');     // a sub-folder

L'appel brut, qui renvoie du texte que vous devez analyser :

if (window.WebToApk && typeof window.WebToApk.listFolderEntries === 'function') {
  var entries = JSON.parse(window.WebToApk.listFolderEntries('notes'));
  console.log(entries.length + ' entries');
}

Trouver le fichier de sauvegarde le plus récent et afficher quand il a été créé :

function latestBackup() {
  if (!window.WebToApkFS) return null;
  var backups = window.WebToApkFS.list('backups').filter(function (e) {
    return !e.isDirectory && /\.json$/i.test(e.name);
  });
  backups.sort(function (a, b) { return b.lastModified - a.lastModified; });   // newest first
  return backups[0] || null;
}

var last = latestBackup();
document.getElementById('last-backup').textContent =
  last ? 'Last backup: ' + new Date(last.lastModified).toLocaleString() : 'No backups yet';

Remarques : Un dossier vide, un dossier manquant, un chemin qui est un fichier, un chemin avec .., l'absence de dossier autorisé et une page d'un site externe répondent tous [] - appelez d'abord hasFolderAccess() si vous devez distinguer « pas de dossier ». Les chemins sont relatifs, utilisent /, et ne remontent jamais au-dessus du dossier autorisé.

mkdir pont#

window.WebToApk.mkdir(relativePath: String): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Crée un dossier dans le dossier autorisé, y compris les dossiers parents manquants.

Retour : la CHAÎNE "true" ou "false", de façon synchrone. WebToApkFS.mkdir(path) renvoie un vrai booléen. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) et un dossier autorisé.

Avec l'assistant :

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  var ok = window.WebToApkFS.mkdir('photos/2026/june');   // creates photos, 2026 and june
  if (!ok) console.log('Could not create the folder');
}

L'appel brut :

if (window.WebToApk && typeof window.WebToApk.mkdir === 'function') {
  var made = window.WebToApk.mkdir('backups') === 'true';
}

Créer un dossier daté pour un export et y écrire chaque élément :

function exportAll(items) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
  var dir = 'exports/' + new Date().toISOString().slice(0, 10);   // e.g. exports/2026-09-25
  if (!window.WebToApkFS.mkdir(dir)) {
    alert('Could not create ' + dir + ' (is a file with that name in the way?)');
    return false;
  }
  items.forEach(function (item) {
    window.WebToApkFS.writeText(dir + '/' + item.id + '.json', JSON.stringify(item));
  });
  return true;
}

Remarques : Elle répond "true" quand le dossier existe déjà, on peut donc l'appeler sans risque avant chaque enregistrement. Elle répond "false" quand un FICHIER du même nom gêne, que le chemin contient .., ou qu'aucun dossier n'est autorisé.

readTextFile pont#

window.WebToApk.readTextFile(relativePath: String): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Lit un fichier texte du dossier autorisé en UTF-8.

Retour : une chaîne, de façon synchrone : le texte du fichier, ou "". WebToApkFS.readText(path) renvoie la même chose. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) et un dossier autorisé.

"" signifie « fichier vide » OU « fichier inexistant » ; vérifiez donc avec WebToApkFS.stat() quand la différence compte :

function loadNote(path) {
  if (!window.WebToApkFS) return null;
  var info = window.WebToApkFS.stat(path);    // { ok: true, name, size, ... } or { ok: false, error: 'not-found' }
  if (!info.ok) return null;                  // the file does not exist
  return window.WebToApkFS.readText(path);    // '' here really is an empty file
}

var text = loadNote('notes/today.txt');
document.getElementById('editor').value = text === null ? '' : text;

L'appel brut :

if (window.WebToApk && typeof window.WebToApk.readTextFile === 'function') {
  var raw = window.WebToApk.readTextFile('notes/today.txt');
  console.log(raw.length + ' characters');
}

Importer toutes les notes .txt et .md d'un dossier :

function importNotes(folder) {
  if (!window.WebToApkFS) return [];
  return window.WebToApkFS.list(folder)
    .filter(function (e) { return !e.isDirectory && /\.(txt|md)$/i.test(e.name); })
    .map(function (e) {
      return {
        title: e.name.replace(/\.(txt|md)$/i, ''),
        body: window.WebToApkFS.readText(folder + '/' + e.name)
      };
    });
}

renderNotes(importNotes('notes'));

Remarques : Le fichier entier est lu en mémoire - correct pour des notes et du JSON, pas pour de gros fichiers ; utilisez WebToApkFS.readBytes pour ceux-là. Les octets qui ne sont pas de l'UTF-8 valide reviennent sous forme de caractère de remplacement. Un chemin de dossier, un chemin avec .., l'absence de dossier autorisé, ou une page d'un site externe répondent tous "".

renameEntry pont#

window.WebToApk.renameEntry(relativePath: String, newName: String): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Renomme un fichier ou un dossier dans le dossier autorisé.

Retour : la CHAÎNE "true" ou "false", de façon synchrone. WebToApkFS.rename(path, newName) renvoie un vrai booléen. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) et un dossier autorisé.

newName est un simple nom, pas un chemin - l'entrée reste dans le même dossier :

if (window.WebToApkFS) {
  var ok = window.WebToApkFS.rename('notes/today.txt', 'journal.txt');   // -> notes/journal.txt
  if (!ok) alert('Rename failed. Is there already a file called journal.txt?');
}

L'appel brut :

if (window.WebToApk && typeof window.WebToApk.renameEntry === 'function') {
  var renamed = window.WebToApk.renameEntry('old-folder', 'new-folder') === 'true';
}

Garder la sauvegarde précédente en la renommant avant l'écriture de la nouvelle :

function rotateBackup(json) {
  var fs = window.WebToApkFS;
  if (!fs || !fs.hasAccess()) return false;
  if (fs.stat('backup.json').ok) {
    var stamp = new Date().toISOString().replace(/[:.]/g, '-');   // no characters a file name cannot hold
    fs.rename('backup.json', 'backup-' + stamp + '.json');        // stays in the same folder
  }
  return fs.writeText('backup.json', json);
}

Remarques : Elle échoue quand une autre entrée porte déjà newName, quand newName est vide ou contient / ou \, quand le chemin n'existe pas, ou qu'aucun dossier n'est autorisé. Pour déplacer un fichier vers un autre dossier, écrivez-le là-bas et supprimez l'ancien.

requestFolderAccess pont#

window.WebToApk.requestFolderAccess(requestId: String)

Ouvre le sélecteur de dossier du système et conserve l'accès en lecture/écriture au dossier choisi. La réponse arrive sur `WebToApkOnFolderAccessResult` ; un seul sélecteur peut être ouvert à la fois, donc une demande faite pendant qu'un autre est ouvert reçoit aussitôt `error:"busy"` et celui qui est ouvert garde sa propre réponse.

Exemple

Ouvre le sélecteur de dossier du système pour que l'utilisateur donne un dossier à l'application. Les pages utilisent normalement WebToApkFS.requestAccess(), qui enveloppe cet appel dans une Promise.

Retour : rien. La réponse revient plus tard par un callback interne que possède WebToApkFS ; WebToApkFS.requestAccess() se résout avec { success, folderUri, requestId }, plus error quand success est faux. Nécessite : cochez Native Folder Access (SAF) à l'étape 4 (Accès) lors de la compilation.

Demander depuis un appui sur un bouton, avec l'assistant :

document.getElementById('choose-folder').addEventListener('click', async function () {
  if (!window.WebToApkFS) {
    alert('Folder access is not available in this app.');
    return;
  }
  var r = await window.WebToApkFS.requestAccess();
  if (r.success) {
    console.log('Folder granted:', r.folderUri);   // content://.../tree/...
  } else {
    console.log('No folder chosen');                 // user cancelled, or Android refused the grant
  }
});

Ne demander que la première fois, puis préparer le sous-dossier propre à l'application :

async function ensureFolder() {
  if (!window.WebToApkFS || !window.WebToApkFS.isAvailable()) return false;
  if (window.WebToApkFS.hasAccess()) return true;          // granted on an earlier run
  var r = await window.WebToApkFS.requestAccess();
  if (!r.success) return false;
  window.WebToApkFS.mkdir('exports');                       // true even if it already exists
  return true;
}

document.getElementById('export').addEventListener('click', async function () {
  if (!(await ensureFolder())) return;
  window.WebToApkFS.writeText('exports/list.csv', buildCsv());
});

Dire exactement à l'utilisateur pourquoi aucun dossier n'a été connecté, avec les vrais codes error :

var folderMessages = {
  cancelled: 'No folder was chosen.',
  not_persisted: 'Android would not keep access to that folder. Please choose another one.',
  busy: 'The folder picker is already open.',
  disabled: 'This app was built without folder access.',
  foreign_origin: 'A folder can only be connected from the app\'s own pages.',
  no_picker: 'This phone has no folder picker.'
};

document.getElementById('choose-folder').addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  var r = await window.WebToApkFS.requestAccess();
  document.getElementById('status').textContent = r.success
    ? 'Folder connected.'
    : (folderMessages[r.error] || 'Could not connect a folder.');
});

Remarques : N'appelez pas vous-même la méthode brute : sa seule réponse est le callback que l'assistant écoute. success vaut false quand l'utilisateur annule (error:'cancelled'), quand Android ne conserve pas l'autorisation ('not_persisted'), et aussitôt quand la compilation n'a pas activé l'option ('disabled') ou que la page à l'écran n'appartient pas à l'application ('foreign_origin'). L'autorisation est conservée entre les redémarrages de l'application ; choisir de nouveau remplace le dossier. Un seul sélecteur peut être ouvert à la fois - une demande faite pendant qu'un autre est ouvert reçoit aussitôt error:'busy', et celui qui est ouvert garde sa propre réponse.

writeTextFile pont#

window.WebToApk.writeTextFile(relativePath: String, content: String): String

Accès aux dossiers conforme à Play, via ACTION_OPEN_DOCUMENT_TREE avec des autorisations d'URI persistantes. Utilisation en JavaScript : window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." ou "" window.WebToApk.clearFolderAccess() // libère l'accès persistant Callback d'Android → JS : window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4e argument, "" en cas de succès) indique pourquoi une demande a échoué : "disabled", "foreign_origin", "busy" (un sélecteur est déjà ouvert pour une demande précédente - celle-ci reçoit quand même sa propre réponse), "cancelled", "not_persisted", "no_picker".

Décrit par son groupe, Native Folder Access (SAF) Bridge, plutôt que séparément.

Exemple

Écrit un fichier texte (UTF-8) dans le dossier autorisé, en le créant ou en remplaçant son contenu.

Retour : la CHAÎNE "true" ou "false", de façon synchrone. WebToApkFS.writeText(path, text) renvoie un vrai booléen. Nécessite : Native Folder Access (SAF) coché à l'étape 4 (Accès) et un dossier autorisé.

Avec l'assistant :

function saveNote(path, text) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) {
    alert('Choose a folder first.');
    return false;
  }
  var ok = window.WebToApkFS.writeText(path, text);   // missing parent folders are created
  if (!ok) alert('Could not save ' + path);
  return ok;
}

saveNote('notes/2026/today.txt', document.getElementById('editor').value);

L'appel brut, dont la réponse est du texte :

if (window.WebToApk && typeof window.WebToApk.writeTextFile === 'function') {
  var saved = window.WebToApk.writeTextFile('data/settings.json', JSON.stringify({ theme: 'dark' })) === 'true';
}

Ajouter une ligne à un journal quotidien - l'écriture remplace le fichier, il faut donc le lire d'abord :

function appendLog(line) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
  var now = new Date().toISOString();
  var path = 'logs/' + now.slice(0, 10) + '.log';          // one file per day
  var old = window.WebToApkFS.readText(path);              // '' the first time
  return window.WebToApkFS.writeText(path, old + now + ' ' + line + '\n');
}

appendLog('Order #1042 marked as shipped');

Remarques : Un fichier existant est entièrement remplacé. Elle échoue quand le chemin est un dossier existant, contient .., ou qu'aucun dossier n'est autorisé. Le type de fichier vient de l'extension (.json, .txt, .md…), donc le nom que vous donnez est le nom sur le disque.

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.