# Files: a folder the user granted - JavaScript bridge API

> A directory chosen once, readable and writable afterwards, with permission that survives reboots.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/files-folder

### `WebToApkFS.clearAccess`

```js
WebToApkFS.clearAccess()
```

**Example**

Forgets the granted folder and gives the permission back to Android.

**Returns:** nothing (synchronous). **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Always offer a way to disconnect next to the folder's name:

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

One button that connects or disconnects, depending on the current state:

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

Switch accounts: forget the folder with the rest of the signed-in state:

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

**Notes:** The files stay where they are; the app simply cannot reach them any more. To use a folder again, call `WebToApkFS.requestAccess()` from a tap.

### `WebToApkFS.delete`

```js
WebToApkFS.delete(relativePath, options)
```

**Example**

Deletes a file or folder inside the granted folder.

**Returns:** a boolean, synchronously: `true` when it was deleted. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build, and a granted folder.

Delete one file, after asking:

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

Delete a folder and everything inside it - pass `{ recursive: true }`:

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

Clean up exports older than 30 days, file by file:

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

**Notes:** Without `{ recursive: true }` a folder is deleted only when it is empty. The granted folder itself (`''`) can never be deleted. Deletion is immediate and there is no trash, so always confirm with the user first.

### `WebToApkFS.getAccessUri`

```js
WebToApkFS.getAccessUri()
```

**Example**

Gives the address of the granted folder, so you can show the user where files go.

**Returns:** a string, synchronously: a tree URI such as `content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes`, or `''` when no folder is granted. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Show a readable folder name next to a Disconnect button:

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

Notice that the user picked a different folder since the last visit, and rebuild what you cached about the old one:

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

A Disconnect button that forgets the folder and confirms it is gone:

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

**Notes:** The file methods do not take this URI - they take paths relative to the granted folder (`'notes/today.txt'`). The URI format belongs to the Files provider the user picked from, so only use it for display.

### `WebToApkFS.hasAccess`

```js
WebToApkFS.hasAccess()
```

**Example**

Tells you whether the user has already given the app a folder.

**Returns:** a boolean, synchronously. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Show "Choose folder" or the file list, depending on the answer:

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

Autosave only while a folder is connected, and say so when it is not:

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

On start-up, reopen the last note when a folder is remembered, otherwise show the welcome screen:

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

**Notes:** It only says the app remembers a folder. If the user deleted or moved that folder, `list('')` returns `[]` - offer "Choose folder" again. `window.WebToApkFS` appears when the page finishes loading; during load use the raw `window.WebToApk.hasFolderAccess() === 'true'`.

### `WebToApkFS.isAvailable`

```js
WebToApkFS.isAvailable()
```

**Example**

Tells you whether this app can use a user-granted folder at all.

**Returns:** a boolean, synchronously: `true` when the build has folder access switched on. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build - without it (and without **Secure Keys**) `window.WebToApkFS` is not added to the page; with only Secure Keys it exists and answers `false`.

`window.WebToApkFS` is added before your page's first script runs; on WebViews older than about 2021 it arrives only when the page finishes loading. To cover those too, wait for it briefly before you decide:

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

Inside a button tap the page has long finished loading, so a direct check is enough:

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

Turn the two checks into one state that drives the whole folder screen:

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

**Notes:** This says nothing about whether a folder is chosen yet - use `WebToApkFS.hasAccess()` for that. Before the page has loaded, the raw `window.WebToApk.isFolderAccessEnabled()` (string `"true"`/`"false"`) answers the same question.

### `WebToApkFS.list`

```js
WebToApkFS.list(relativePath)
```

**Example**

Lists the files and sub-folders inside a folder of the granted folder.

**Returns:** an array, synchronously: `[{ name, uri, isDirectory, length, lastModified }]` (`length` in bytes, `lastModified` in ms since 1970). **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build, and a granted folder.

A simple file browser (`''` is the granted folder itself):

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

Walk every sub-folder to collect all Markdown notes (paths you can pass to `readText`):

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

Find the newest backup and how much space the backups take:

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

**Notes:** Empty folder, missing folder, a path that is a file, a path with `..`, and no granted folder all return `[]`. The list is not sorted. Each `uri` also works with `WebToApkFS.stat()` and `WebToApkFS.readBytes()`.

### `WebToApkFS.mkdir`

```js
WebToApkFS.mkdir(relativePath)
```

**Example**

Creates a folder inside the granted folder, including any missing parent folders.

**Returns:** a boolean, synchronously: `true` when the folder exists afterwards. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build, and a granted folder.

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

Set up a project's folder layout on first use, and report any folder that could not be made:

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

A "New folder" button that warns when the name is already taken (because `mkdir` answers `true` for an existing folder):

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

**Notes:** It returns `true` if the folder already exists, so calling it before each save is fine. It returns `false` when a FILE with that name is in the way, the path contains `..`, or no folder is granted. `WebToApkFS.writeText()` already creates missing parents, so you only need `mkdir` for empty folders.

### `WebToApkFS.readText`

```js
WebToApkFS.readText(relativePath)
```

**Example**

Reads a text file from the granted folder as UTF-8.

**Returns:** a string, synchronously: the file's text, or `''`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build, and a granted folder.

`''` means "empty file" OR "no such file". Use `WebToApkFS.stat()` when you must know which:

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

Open a note into a text box:

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

Import a CSV the user dropped into the folder:

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

**Notes:** The whole file is loaded at once - for big or binary files use `WebToApkFS.readBytes()`. Bytes that are not valid UTF-8 come back as the replacement character. A folder, a path with `..`, or no granted folder also returns `''`.

### `WebToApkFS.rename`

```js
WebToApkFS.rename(relativePath, newName)
```

**Example**

Renames a file or folder inside the granted folder.

**Returns:** a boolean, synchronously. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build, and a granted folder.

`newName` is only the new name, not a path. The entry stays in the same folder:

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

Rename without clashing: try "name (2)", "name (3)" … until a free name is found:

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

Save safely: write a temporary file first, then swap it in (rename refuses a name that is taken, so the old file is deleted just before):

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

**Notes:** It returns `false` when another entry already has `newName`, when `newName` is empty, `.`/`..`, or contains `/` or `\`, when the path does not exist, or when no folder is granted. To move a file between folders, read it, write it in the new place, then delete the old one.

### `WebToApkFS.requestAccess`

```js
WebToApkFS.requestAccess()
```

**Example**

Opens the system folder picker so the user can give the app one folder, kept across restarts.

**Returns:** a Promise that always resolves (never rejects) with `{ success, folderUri, requestId }`, plus `error` when `success` is false. `success` is a boolean; `folderUri` is the granted `content://…/tree/…` address, or `''`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Ask from a button the user taps, and show which folder is connected:

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

Only ask when no folder is granted yet:

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

A "Change folder" button that explains every way the request can fail:

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

**Notes:** Call it from a tap, never on page load - the picker covers the app. `success: false` comes with `error`: `'cancelled'` (the user backed out), `'not_persisted'` (Android would not keep the grant), `'disabled'`, `'busy'` (a picker is already open for an earlier request - that one still gets its own answer), `'no_picker'` or `'foreign_origin'`. Choosing again replaces the earlier folder.

### `WebToApkFS.writeText`

```js
WebToApkFS.writeText(relativePath, content)
```

**Example**

Writes a UTF-8 text file into the granted folder, creating it (and missing parent folders) or replacing it.

**Returns:** a boolean, synchronously: `true` when the file was written. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build, and a granted folder.

Save from a button:

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

Save an object as JSON:

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

Add a line to a daily log (there is no append, so read, add, and write back):

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

**Notes:** An existing file is replaced completely - there is no append. `content` is turned into a string (`null` becomes `''`). It returns `false` when the path is an existing folder, contains `..`, or no folder is granted. The file type is taken from the extension, so the name on disk is exactly the name you gave.

### `clearFolderAccess`

```js
window.WebToApk.clearFolderAccess()
```

Releases the persisted folder grant. Refused (no-op) for a foreign page.

**Example**

Forgets the granted folder and gives the permission back to Android.

**Returns:** nothing (synchronous). `WebToApkFS.clearAccess()` does the same. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access); with the switch off it does nothing.

A "Disconnect" button:

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

On sign-out, forget the folder too, so the next person on this phone chooses their own:

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

Forget a folder that was moved or deleted in the Files app, then offer to choose again (the app writes `myapp-folder.json` when a folder is first connected):

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

**Notes:** Files in the folder are NOT deleted; the app just cannot reach them any more. After this, `hasFolderAccess()` answers `"false"` and every file method fails until the user chooses a folder again.

### `deleteEntry`

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

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Deletes a file or folder inside the granted folder.

**Returns:** the STRING `"true"` or `"false"`, synchronously. `WebToApkFS.delete(path, { recursive })` returns a real boolean. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access) and a granted folder.

With the helper, after asking the user:

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

The raw call - the second argument is the STRING `'true'` or `'false'`:

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

Keep only the five newest backups and delete the rest:

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

**Notes:** A folder that is not empty is only deleted with recursive `'true'`. The granted folder itself (`''`) can never be deleted. Deleting is real and immediate: there is no trash.

### `getFolderAccessUri`

```js
window.WebToApk.getFolderAccessUri(): String
```

The granted folder's `content://` tree URI, or "" (none, disabled, or a foreign page).

**Example**

Gives the address of the folder the user granted, so you can show which folder is connected.

**Returns:** a string, synchronously: a tree URI such as `content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes`, or `""` when no folder is granted or the switch is off. `WebToApkFS.getAccessUri()` returns the same string. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access).

Turn the URI into a name the user recognises:

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

Reload the file list only when the user really picked a different folder:

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

Say whether the folder is on the phone, on an SD card, or in another storage app:

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

**Notes:** You never pass this URI back to the file methods - they all take paths relative to this folder. Show the name next to a "Disconnect" button so the user knows where files go.

### `hasFolderAccess`

```js
window.WebToApk.hasFolderAccess(): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Tells you whether the user has already given the app a folder.

**Returns:** the STRING `"true"` or `"false"` (not a boolean), synchronously. `WebToApkFS.hasAccess()` gives a real boolean. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access); without it the answer is always `"false"`.

The raw method exists from the very first line of the page, so it is the right check while the page is still loading (`WebToApkFS` is added only when loading finishes):

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

Check before a save and open the picker only when no folder is connected yet:

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

An autosave timer that writes only when a folder is connected and never opens the picker itself:

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

**Notes:** Compare with `=== 'true'`; the string `"false"` is truthy. It only says the app remembers a folder. If that folder was deleted or moved in the Files app, `listFolderEntries('')` answers `[]` - ask the user to choose it again.

### `isFolderAccessEnabled`

```js
window.WebToApk.isFolderAccessEnabled(): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Tells you whether this app was built with folder access switched on at all.

**Returns:** the STRING `"true"` or `"false"`, synchronously. **Needs:** nothing to call it; it answers `"true"` only when **Native Folder Access (SAF)** was ticked in Step 4 (Access).

Use it to decide whether to show the folder feature. `WebToApkFS.isAvailable()` answers the same question as a boolean once the page has loaded:

```js
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
}
```

Drive a settings row through its three states (not in this build, not connected, connected):

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

Mark the page before it is drawn, so CSS can show folder-only controls without a flash:

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

**Notes:** This says nothing about whether a folder is chosen yet - use `hasFolderAccess()` for that. In a browser `window.WebToApk` does not exist, so the check above answers `false` without throwing.

### `listFolderEntries`

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

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Lists the files and sub-folders inside a folder of the granted folder.

**Returns:** a JSON STRING, synchronously: an array of `{ name, uri, isDirectory, length, lastModified }` (`length` in bytes, `lastModified` in ms since 1970). `WebToApkFS.list(path)` parses it for you. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access) and a granted folder.

With the helper (`''` is the granted folder itself):

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

The raw call, which returns text you must parse:

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

Find the newest backup file and show when it was made:

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

**Notes:** An empty folder, a missing folder, a path that is a file, a path with `..`, no granted folder, and a page from a foreign website all answer `[]` - call `hasFolderAccess()` first if you need to tell "no folder" apart. Paths are relative, use `/`, and never go above the granted folder.

### `mkdir`

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

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Creates a folder inside the granted folder, including any missing parent folders.

**Returns:** the STRING `"true"` or `"false"`, synchronously. `WebToApkFS.mkdir(path)` returns a real boolean. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access) and a granted folder.

With the helper:

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

The raw call:

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

Make a dated folder for one export and write every item into it:

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

**Notes:** It answers `"true"` when the folder already exists, so it is safe to call before every save. It answers `"false"` when a FILE with that name is in the way, the path contains `..`, or no folder is granted.

### `readTextFile`

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

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Reads a text file from the granted folder as UTF-8.

**Returns:** a string, synchronously: the file's text, or `""`. `WebToApkFS.readText(path)` returns the same. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access) and a granted folder.

`""` means "empty file" OR "no such file", so check with `WebToApkFS.stat()` when the difference matters:

```js
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;
```

The raw call:

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

Import every `.txt` and `.md` note from a folder:

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

**Notes:** The whole file is read into memory - fine for notes and JSON, not for large files; use `WebToApkFS.readBytes` for those. Bytes that are not valid UTF-8 come back as the replacement character. A folder path, a path with `..`, no granted folder, or a page from a foreign website all answer `""`.

### `renameEntry`

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

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Renames a file or folder inside the granted folder.

**Returns:** the STRING `"true"` or `"false"`, synchronously. `WebToApkFS.rename(path, newName)` returns a real boolean. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access) and a granted folder.

`newName` is a plain name, not a path - the entry stays in the same folder:

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

The raw call:

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

Keep the previous backup by renaming it before the new one is written:

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

**Notes:** It fails when another entry already has `newName`, when `newName` is empty or contains `/` or `\`, when the path does not exist, or when no folder is granted. To move a file to another folder, write it there and delete the old one.

### `requestFolderAccess`

```js
window.WebToApk.requestFolderAccess(requestId: String)
```

Opens the system folder picker and persists read/write access to the chosen folder. The answer arrives on `WebToApkOnFolderAccessResult`; only one picker can be open at a time, so a request made while one is open is answered at once with `error:"busy"` and the open one keeps its own answer.

**Example**

Opens the system folder picker so the user can give the app one folder. Pages normally use `WebToApkFS.requestAccess()`, which wraps this call in a Promise.

**Returns:** nothing. The answer comes back later through an internal callback that `WebToApkFS` owns; `WebToApkFS.requestAccess()` resolves with `{ success, folderUri, requestId }`, plus `error` when `success` is false. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Ask from a button tap, with the helper:

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

Ask only the first time, then set up the app's own sub-folder:

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

Tell the user exactly why no folder was connected, using the real `error` codes:

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

**Notes:** Do not call the raw method yourself: its only answer is the callback the helper listens to. `success` is `false` when the user cancels (`error:'cancelled'`), when Android will not keep the grant (`'not_persisted'`), and at once when the build did not enable the switch (`'disabled'`) or the page on screen is not the app's own (`'foreign_origin'`). The grant is kept across app restarts; choosing again replaces the folder. Only one picker can be open at a time - a request made while one is open is answered at once with `error:'busy'`, and the open one keeps its own answer.

### `writeTextFile`

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

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

**Example**

Writes a text file (UTF-8) into the granted folder, creating it or replacing its content.

**Returns:** the STRING `"true"` or `"false"`, synchronously. `WebToApkFS.writeText(path, text)` returns a real boolean. **Needs:** **Native Folder Access (SAF)** ticked in Step 4 (Access) and a granted folder.

With the helper:

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

The raw call, whose answer is text:

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

Append a line to a daily log - writing replaces the file, so read it first:

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

**Notes:** An existing file is replaced completely. It fails when the path is an existing folder, contains `..`, or no folder is granted. The file type comes from the extension (`.json`, `.txt`, `.md`…), so the name you give is the name on disk.

