Crear APK gratis

API del puente de JavaScript

Archivos: una carpeta concedida por el usuario

Un directorio elegido una vez, legible y escribible después, con un permiso que sobrevive a los reinicios.

WebToApkFS.clearAccess ayudante#

WebToApkFS.clearAccess()

Ejemplo

Olvida la carpeta concedida y devuelve el permiso a Android.

Devuelve: nada (síncrono). Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar.

Ofrece siempre una forma de desconectar junto al nombre de la carpeta:

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 botón que conecta o desconecta según el estado actual:

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

Cambia de cuenta: olvida la carpeta junto con el resto del estado de la sesión:

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

Notas: Los archivos siguen donde están; la app simplemente ya no puede acceder a ellos. Para volver a usar una carpeta, llama a WebToApkFS.requestAccess() desde un toque.

WebToApkFS.delete ayudante#

WebToApkFS.delete(relativePath, options)

Ejemplo

Elimina un archivo o carpeta dentro de la carpeta concedida.

Devuelve: un booleano, de forma síncrona: true cuando se eliminó. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar y una carpeta concedida.

Elimina un archivo, después de preguntar:

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

Elimina una carpeta y todo lo que hay dentro: pasa { 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');
}

Limpia las exportaciones de más de 30 días, archivo por archivo:

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

Notas: Sin { recursive: true } una carpeta solo se elimina cuando está vacía. La propia carpeta concedida ('') nunca se puede eliminar. La eliminación es inmediata y no hay papelera, así que confirma siempre con el usuario primero.

WebToApkFS.getAccessUri ayudante#

WebToApkFS.getAccessUri()

Ejemplo

Da la dirección de la carpeta concedida, para que puedas mostrar al usuario adónde van los archivos.

Devuelve: una cadena, de forma síncrona: un URI de árbol como content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes, o '' cuando no hay carpeta concedida. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar.

Muestra un nombre de carpeta legible junto a un botón Desconectar:

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

Detecta que el usuario eligió una carpeta distinta desde la última visita y reconstruye lo que guardaste en caché sobre la anterior:

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 botón Desconectar que olvida la carpeta y confirma que ya no está:

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

Notas: Los métodos de archivos no toman este URI: toman rutas relativas a la carpeta concedida ('notes/today.txt'). El formato del URI pertenece al proveedor de Archivos desde el que eligió el usuario, así que úsalo solo para mostrar.

WebToApkFS.hasAccess ayudante#

WebToApkFS.hasAccess()

Ejemplo

Indica si el usuario ya ha dado una carpeta a la app.

Devuelve: un booleano, de forma síncrona. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar.

Muestra "Elegir carpeta" o la lista de archivos, según la respuesta:

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

Guarda automáticamente solo mientras haya una carpeta conectada e indícalo cuando no la haya:

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

Al iniciar, vuelve a abrir la última nota cuando se recuerda una carpeta; si no, muestra la pantalla de bienvenida:

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

Notas: Solo dice que la app recuerda una carpeta. Si el usuario eliminó o movió esa carpeta, list('') devuelve []: ofrece "Elegir carpeta" de nuevo. window.WebToApkFS aparece cuando la página termina de cargarse; durante la carga usa el sin procesar window.WebToApk.hasFolderAccess() === 'true'.

WebToApkFS.isAvailable ayudante#

WebToApkFS.isAvailable()

Ejemplo

Indica si esta app puede usar una carpeta concedida por el usuario.

Devuelve: un booleano, de forma síncrona: true cuando la compilación tiene activado el acceso a carpetas. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar; sin ello (y sin Claves seguras) window.WebToApkFS no se añade a la página; con solo Claves seguras existe y responde false.

window.WebToApkFS se añade antes de que se ejecute el primer script de tu página; en WebViews anteriores a 2021 aproximadamente llega solo cuando la página termina de cargarse. Para cubrirlos también, espéralo brevemente antes de decidir:

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

Dentro de un toque en un botón la página ya terminó de cargarse hace tiempo, así que basta una comprobación directa:

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

Convierte las dos comprobaciones en un solo estado que gobierne toda la pantalla de carpetas:

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

Notas: Esto no dice nada sobre si ya hay una carpeta elegida; usa WebToApkFS.hasAccess() para eso. Antes de que se cargue la página, el window.WebToApk.isFolderAccessEnabled() sin procesar (cadena "true"/"false") responde la misma pregunta.

WebToApkFS.list ayudante#

WebToApkFS.list(relativePath)

Ejemplo

Lista los archivos y subcarpetas dentro de una carpeta de la carpeta concedida.

Devuelve: un array, de forma síncrona: [{ name, uri, isDirectory, length, lastModified }] (length en bytes, lastModified en ms desde 1970). Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar y una carpeta concedida.

Un explorador de archivos sencillo ('' es la propia carpeta concedida):

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

Recorre cada subcarpeta para reunir todas las notas Markdown (rutas que puedes pasar a 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';
}

Encuentra la copia de seguridad más reciente y cuánto espacio ocupan las copias:

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

Notas: Una carpeta vacía, una carpeta que no existe, una ruta que es un archivo, una ruta con .. y ninguna carpeta concedida devuelven todas []. La lista no está ordenada. Cada uri también funciona con WebToApkFS.stat() y WebToApkFS.readBytes().

WebToApkFS.mkdir ayudante#

WebToApkFS.mkdir(relativePath)

Ejemplo

Crea una carpeta dentro de la carpeta concedida, incluidas las carpetas superiores que falten.

Devuelve: un booleano, de forma síncrona: true cuando la carpeta existe después. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar y una carpeta concedida.

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

Prepara la estructura de carpetas de un proyecto en el primer uso e informa de cualquier carpeta que no se pudo crear:

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 botón "Nueva carpeta" que avisa cuando el nombre ya está en uso (porque mkdir responde true para una carpeta existente):

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

Notas: Devuelve true si la carpeta ya existe, así que llamarlo antes de cada guardado está bien. Devuelve false cuando hay un ARCHIVO con ese nombre en el camino, la ruta contiene .. o no se ha concedido ninguna carpeta. WebToApkFS.writeText() ya crea las carpetas superiores que falten, así que solo necesitas mkdir para carpetas vacías.

WebToApkFS.readText ayudante#

WebToApkFS.readText(relativePath)

Ejemplo

Lee un archivo de texto de la carpeta concedida como UTF-8.

Devuelve: una cadena, de forma síncrona: el texto del archivo, o ''. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar y una carpeta concedida.

'' significa "archivo vacío" O "no existe ese archivo". Usa WebToApkFS.stat() cuando necesites saber cuál:

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

Abre una nota en un cuadro de texto:

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

Importa un CSV que el usuario dejó en la carpeta:

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

Notas: El archivo completo se carga de una vez; para archivos grandes o binarios usa WebToApkFS.readBytes(). Los bytes que no son UTF-8 válido vuelven como el carácter de reemplazo. Una carpeta, una ruta con .. o ninguna carpeta concedida también devuelve ''.

WebToApkFS.rename ayudante#

WebToApkFS.rename(relativePath, newName)

Ejemplo

Renombra un archivo o carpeta dentro de la carpeta concedida.

Devuelve: un booleano, de forma síncrona. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar y una carpeta concedida.

newName es solo el nombre nuevo, no una ruta. La entrada se queda en la misma carpeta:

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

Renombra sin choques: prueba "nombre (2)", "nombre (3)" … hasta encontrar un nombre 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');

Guarda con seguridad: escribe primero un archivo temporal y luego cámbialo (renombrar rechaza un nombre que ya existe, así que el archivo antiguo se elimina justo antes):

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

Notas: Devuelve false cuando otra entrada ya tiene newName, cuando newName está vacío, es ./.. o contiene / o \, cuando la ruta no existe o cuando no se ha concedido ninguna carpeta. Para mover un archivo entre carpetas, léelo, escríbelo en el nuevo lugar y luego elimina el antiguo.

WebToApkFS.requestAccess ayudante#

WebToApkFS.requestAccess()

Ejemplo

Abre el selector de carpetas del sistema para que el usuario dé a la app una carpeta, que se conserva entre reinicios.

Devuelve: una Promise que siempre se resuelve (nunca rechaza) con { success, folderUri, requestId }, más error cuando success es falso. success es un booleano; folderUri es la dirección content://…/tree/… concedida, o ''. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar.

Pide permiso desde un botón que toque el usuario y muestra qué carpeta está conectada:

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

Pregunta solo cuando aún no se haya concedido ninguna carpeta:

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

Un botón "Cambiar carpeta" que explica cada forma en que puede fallar la solicitud:

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

Notas: Llámalo desde un toque, nunca al cargar la página: el selector cubre la app. success: false viene con error: 'cancelled' (el usuario se echó atrás), 'not_persisted' (Android no quiso conservar la concesión), 'disabled', 'busy' (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), 'no_picker' o 'foreign_origin'. Elegir de nuevo reemplaza la carpeta anterior.

WebToApkFS.writeText ayudante#

WebToApkFS.writeText(relativePath, content)

Ejemplo

Escribe un archivo de texto UTF-8 en la carpeta concedida, creándolo (y las carpetas superiores que falten) o reemplazándolo.

Devuelve: un booleano, de forma síncrona: true cuando se escribió el archivo. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar y una carpeta concedida.

Guardar desde un botón:

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

Guarda un objeto como JSON:

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

Añade una línea a un registro diario (no hay añadir: lee, agrega y vuelve a escribir):

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

Notas: Un archivo existente se reemplaza por completo; no hay añadir. content se convierte en cadena (null pasa a ser ''). Devuelve false cuando la ruta es una carpeta existente, contiene .. o no se ha concedido ninguna carpeta. El tipo de archivo se toma de la extensión, así que el nombre en el disco es exactamente el que diste.

clearFolderAccess puente#

window.WebToApk.clearFolderAccess()

Libera la concesión de carpeta persistente. Se rechaza (sin efecto) para una página ajena.

Ejemplo

Olvida la carpeta concedida y devuelve el permiso a Android.

Devuelve: nada (síncrono). WebToApkFS.clearAccess() hace lo mismo. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso); con el interruptor desactivado no hace nada.

Un botón "Desconectar":

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

Al cerrar sesión, olvida también la carpeta, para que la siguiente persona en este teléfono elija la suya:

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

Olvida una carpeta que se movió o eliminó en la app Archivos y luego ofrece elegir de nuevo (la app escribe myapp-folder.json cuando se conecta una carpeta por primera vez):

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

Notas: Los archivos de la carpeta NO se eliminan; la app simplemente ya no puede acceder a ellos. Después de esto, hasFolderAccess() responde "false" y todos los métodos de archivos fallan hasta que el usuario elija una carpeta de nuevo.

deleteEntry puente#

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

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Elimina un archivo o carpeta dentro de la carpeta concedida.

Devuelve: la CADENA "true" o "false", de forma síncrona. WebToApkFS.delete(path, { recursive }) devuelve un booleano real. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso) y una carpeta concedida.

Con el asistente, después de preguntar al usuario:

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

La llamada sin procesar: el segundo argumento es la CADENA 'true' o 'false':

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

Conserva solo las cinco copias de seguridad más recientes y elimina el resto:

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

Notas: Una carpeta que no está vacía solo se elimina con 'true' recursivo. La propia carpeta concedida ('') nunca se puede eliminar. Eliminar es real e inmediato: no hay papelera.

getFolderAccessUri puente#

window.WebToApk.getFolderAccessUri(): String

El URI de árbol `content://` de la carpeta concedida, o "" (ninguna, desactivado o una página ajena).

Ejemplo

Da la dirección de la carpeta que concedió el usuario, para que puedas mostrar qué carpeta está conectada.

Devuelve: una cadena, de forma síncrona: un URI de árbol como content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes, o "" cuando no hay carpeta concedida o el interruptor está desactivado. WebToApkFS.getAccessUri() devuelve la misma cadena. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso).

Convierte el URI en un nombre que el usuario reconozca:

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

Recarga la lista de archivos solo cuando el usuario realmente haya elegido otra carpeta:

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

Indica si la carpeta está en el teléfono, en una tarjeta SD o en otra app de almacenamiento:

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

Notas: Nunca pases este URI de vuelta a los métodos de archivos: todos toman rutas relativas a esta carpeta. Muestra el nombre junto a un botón "Desconectar" para que el usuario sepa adónde van los archivos.

hasFolderAccess puente#

window.WebToApk.hasFolderAccess(): String

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Indica si el usuario ya ha dado una carpeta a la app.

Devuelve: la CADENA "true" o "false" (no un booleano), de forma síncrona. WebToApkFS.hasAccess() da un booleano real. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso); sin ello la respuesta siempre es "false".

El método sin procesar existe desde la primera línea de la página, así que es la comprobación correcta mientras la página aún se carga (WebToApkFS se añade solo cuando termina la carga):

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

Comprueba antes de guardar y abre el selector solo cuando aún no hay ninguna carpeta conectada:

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 temporizador de guardado automático que escribe solo cuando hay una carpeta conectada y nunca abre el selector por sí mismo:

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

Notas: Compara con === 'true'; la cadena "false" es verdadera. Solo dice que la app recuerda una carpeta. Si esa carpeta se eliminó o movió en la app Archivos, listFolderEntries('') responde []: pide al usuario que la elija de nuevo.

isFolderAccessEnabled puente#

window.WebToApk.isFolderAccessEnabled(): String

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Indica si esta app se compiló con el acceso a carpetas activado.

Devuelve: la CADENA "true" o "false", de forma síncrona. Requiere: nada para llamarlo; responde "true" solo cuando Acceso nativo a carpetas (SAF) se marcó en el Paso 4 (Acceso).

Úsalo para decidir si mostrar la función de carpetas. WebToApkFS.isAvailable() responde la misma pregunta como un booleano una vez cargada la página:

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
}

Recorre una fila de ajustes por sus tres estados (no está en esta compilación, no conectada, conectada):

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

Marca la página antes de dibujarla, para que el CSS pueda mostrar controles exclusivos de carpetas sin parpadeo:

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

Notas: Esto no dice nada sobre si ya hay una carpeta elegida; usa hasFolderAccess() para eso. En un navegador window.WebToApk no existe, así que la comprobación anterior responde false sin lanzar errores.

listFolderEntries puente#

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

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Lista los archivos y subcarpetas dentro de una carpeta de la carpeta concedida.

Devuelve: una CADENA JSON, de forma síncrona: un array de { name, uri, isDirectory, length, lastModified } (length en bytes, lastModified en ms desde 1970). WebToApkFS.list(path) la analiza por ti. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso) y una carpeta concedida.

Con el asistente ('' es la propia carpeta concedida):

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

La llamada sin procesar, que devuelve texto que debes analizar:

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

Encuentra el archivo de copia de seguridad más reciente y muestra cuándo se hizo:

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

Notas: Una carpeta vacía, una carpeta que no existe, una ruta que es un archivo, una ruta con .., ninguna carpeta concedida y una página de un sitio web ajeno responden todas []; llama primero a hasFolderAccess() si necesitas distinguir "sin carpeta". Las rutas son relativas, usan / y nunca suben por encima de la carpeta concedida.

mkdir puente#

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

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Crea una carpeta dentro de la carpeta concedida, incluidas las carpetas superiores que falten.

Devuelve: la CADENA "true" o "false", de forma síncrona. WebToApkFS.mkdir(path) devuelve un booleano real. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso) y una carpeta concedida.

Con el asistente:

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

La llamada en bruto:

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

Crea una carpeta con fecha para una exportación y escribe cada elemento en ella:

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

Notas: Responde "true" cuando la carpeta ya existe, así que es seguro llamarlo antes de cada guardado. Responde "false" cuando hay un ARCHIVO con ese nombre en el camino, la ruta contiene .. o no se ha concedido ninguna carpeta.

readTextFile puente#

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

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Lee un archivo de texto de la carpeta concedida como UTF-8.

Devuelve: una cadena, de forma síncrona: el texto del archivo, o "". WebToApkFS.readText(path) devuelve lo mismo. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso) y una carpeta concedida.

"" significa "archivo vacío" O "no existe ese archivo", así que comprueba con WebToApkFS.stat() cuando la diferencia importe:

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;

La llamada en bruto:

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

Importa todas las notas .txt y .md de una carpeta:

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

Notas: El archivo completo se lee en memoria: está bien para notas y JSON, no para archivos grandes; usa WebToApkFS.readBytes para esos. Los bytes que no son UTF-8 válido vuelven como el carácter de reemplazo. Una ruta de carpeta, una ruta con .., ninguna carpeta concedida o una página de un sitio web ajeno responden todas "".

renameEntry puente#

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

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Renombra un archivo o carpeta dentro de la carpeta concedida.

Devuelve: la CADENA "true" o "false", de forma síncrona. WebToApkFS.rename(path, newName) devuelve un booleano real. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso) y una carpeta concedida.

newName es un nombre simple, no una ruta: la entrada se queda en la misma carpeta:

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

La llamada en bruto:

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

Conserva la copia de seguridad anterior renombrándola antes de escribir la nueva:

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

Notas: Falla cuando otra entrada ya tiene newName, cuando newName está vacío o contiene / o \, cuando la ruta no existe o cuando no se ha concedido ninguna carpeta. Para mover un archivo a otra carpeta, escríbelo allí y elimina el antiguo.

requestFolderAccess puente#

window.WebToApk.requestFolderAccess(requestId: String)

Abre el selector de carpetas del sistema y conserva el acceso de lectura/escritura a la carpeta elegida. La respuesta llega a `WebToApkOnFolderAccessResult`; solo puede haber un selector abierto a la vez, así que una solicitud hecha mientras hay uno abierto se responde al instante con `error:"busy"` y el abierto conserva su propia respuesta.

Ejemplo

Abre el selector de carpetas del sistema para que el usuario dé a la app una carpeta. Normalmente las páginas usan WebToApkFS.requestAccess(), que envuelve esta llamada en una Promise.

Devuelve: nada. La respuesta vuelve más tarde a través de un callback interno que posee WebToApkFS; WebToApkFS.requestAccess() se resuelve con { success, folderUri, requestId }, más error cuando success es falso. Requiere: marcar Acceso nativo a carpetas (SAF) en el Paso 4 (Acceso) al compilar.

Pide permiso desde un toque en un botón, con el asistente:

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

Pregunta solo la primera vez y luego prepara la subcarpeta propia de la app:

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

Indica al usuario exactamente por qué no se conectó ninguna carpeta, usando los códigos error reales:

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

Notas: No llames tú mismo al método sin procesar: su única respuesta es el callback que escucha el asistente. success es false cuando el usuario cancela (error:'cancelled'), cuando Android no quiere conservar la concesión ('not_persisted'), y al instante cuando la compilación no activó el interruptor ('disabled') o la página en pantalla no es la propia de la app ('foreign_origin'). La concesión se conserva entre reinicios de la app; elegir de nuevo reemplaza la carpeta. Solo puede haber un selector abierto a la vez: una solicitud hecha mientras hay uno abierto se responde al instante con error:'busy', y el abierto conserva su propia respuesta.

writeTextFile puente#

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

Acceso a carpetas seguro para Play usando ACTION_OPEN_DOCUMENT_TREE con permisos URI persistentes. Uso en JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." o "" window.WebToApk.clearFolderAccess() // libera el acceso persistente Callback de Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4.º argumento, "" si hay éxito) indica por qué falló una solicitud: "disabled", "foreign_origin", "busy" (ya hay un selector abierto para una solicitud anterior; esa sigue recibiendo su propia respuesta), "cancelled", "not_persisted", "no_picker".

Se describe en su grupo, Puente de acceso nativo a carpetas (SAF), y no por separado.

Ejemplo

Escribe un archivo de texto (UTF-8) en la carpeta concedida, creándolo o reemplazando su contenido.

Devuelve: la CADENA "true" o "false", de forma síncrona. WebToApkFS.writeText(path, text) devuelve un booleano real. Requiere: Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso) y una carpeta concedida.

Con el asistente:

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

La llamada sin procesar, cuya respuesta es texto:

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

Añade una línea a un registro diario: escribir reemplaza el archivo, así que léelo primero:

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

Notas: Un archivo existente se reemplaza por completo. Falla cuando la ruta es una carpeta existente, contiene .. o no se ha concedido ninguna carpeta. El tipo de archivo viene de la extensión (.json, .txt, .md…), así que el nombre que das es el nombre en el disco.

Generado a partir del runtime de la app y sus archivos de ejemplo en cada compilación de la documentación. Léelo como Markdown · Todas las familias.

Comprobado con el puente publicado el 2026-10-22.