ساخت APK رایگان

API پل جاوااسکریپت

Files: پوشه‌ای که کاربر مجاز کرده

یک پوشه که یک بار انتخاب می‌شود، بعد از آن قابل خواندن و نوشتن است، با مجوزی که بعد از ریبوت می‌ماند.

WebToApkFS.clearAccess کمک‌کننده#

WebToApkFS.clearAccess()

نمونه

پوشه‌ی مجاز را فراموش می‌کند و مجوز را به Android پس می‌دهد.

مقدار بازگشتی: هیچ (همگام). پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید.

همیشه کنار نام پوشه راهی برای قطع اتصال بگذارید:

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

یک دکمه که بسته به وضعیت فعلی وصل یا قطع می‌کند:

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

تعویض حساب: پوشه را همراه بقیه‌ی وضعیت ورود فراموش کنید:

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

نکته‌ها: فایل‌ها سر جایشان می‌مانند؛ اپلیکیشن فقط دیگر به آن‌ها دسترسی ندارد. برای استفاده‌ی دوباره از یک پوشه، WebToApkFS.requestAccess() را از یک لمس فراخوانی کنید.

WebToApkFS.delete کمک‌کننده#

WebToApkFS.delete(relativePath, options)

نمونه

یک فایل یا پوشه را داخل پوشه‌ی مجاز حذف می‌کند.

مقدار بازگشتی: یک boolean، همگام: true وقتی حذف شد. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید و یک پوشه‌ی مجاز داشته باشید.

یک فایل را بعد از پرسیدن حذف کنید:

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

یک پوشه و همه‌ی محتویاتش را حذف کنید؛ بدهید { 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');
}

خروجی‌های قدیمی‌تر از 30 روز را فایل به فایل پاک کنید:

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

نکته‌ها: بدون { recursive: true } پوشه فقط وقتی خالی باشد حذف می‌شود. خود پوشه‌ی مجاز ('') هرگز حذف نمی‌شود. حذف فوری است و سطل زباله‌ای وجود ندارد، پس همیشه ابتدا از کاربر تأیید بگیرید.

WebToApkFS.getAccessUri کمک‌کننده#

WebToApkFS.getAccessUri()

نمونه

نشانی پوشه‌ی مجاز را می‌دهد تا بتوانید به کاربر نشان دهید فایل‌ها کجا می‌روند.

مقدار بازگشتی: یک رشته، همگام: نشانی درخت مثل content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes، یا '' وقتی پوشه‌ای مجاز نیست. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید.

یک نام پوشه‌ی خوانا را کنار دکمه‌ی Disconnect نشان دهید:

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

متوجه شوید کاربر از آخرین بازدید پوشه‌ی دیگری انتخاب کرده و آنچه را درباره‌ی پوشه‌ی قبلی کش کرده بودید دوباره بسازید:

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

یک دکمه‌ی Disconnect که پوشه را فراموش می‌کند و تأیید می‌کند که رفته است:

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

نکته‌ها: متدهای فایل این URI را نمی‌گیرند؛ آن‌ها مسیرهای نسبت به پوشه‌ی مجاز را می‌گیرند ('notes/today.txt'). قالب URI به ارائه‌دهنده‌ی Files که کاربر از آن انتخاب کرده تعلق دارد، پس فقط برای نمایش از آن استفاده کنید.

WebToApkFS.hasAccess کمک‌کننده#

WebToApkFS.hasAccess()

نمونه

می‌گوید کاربر از قبل به اپلیکیشن پوشه‌ای داده یا نه.

مقدار بازگشتی: یک boolean، همگام. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید.

بسته به پاسخ، «انتخاب پوشه» یا فهرست فایل‌ها را نشان دهید:

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

فقط وقتی پوشه‌ای متصل است ذخیره‌ی خودکار کنید و وقتی نیست بگویید:

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

هنگام شروع، اگر پوشه‌ای به خاطر سپرده شده آخرین یادداشت را دوباره باز کنید، وگرنه صفحه‌ی خوش‌آمد را نشان دهید:

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

نکته‌ها: این فقط می‌گوید اپلیکیشن پوشه‌ای را به خاطر دارد. اگر کاربر آن پوشه را حذف یا جابه‌جا کرده باشد، list('') مقدار [] برمی‌گرداند؛ دوباره «انتخاب پوشه» را پیشنهاد دهید. window.WebToApkFS وقتی بارگذاری صفحه تمام شود ظاهر می‌شود؛ هنگام بارگذاری از متد خام استفاده کنید window.WebToApk.hasFolderAccess() === 'true'.

WebToApkFS.isAvailable کمک‌کننده#

WebToApkFS.isAvailable()

نمونه

می‌گوید این اپلیکیشن اصلاً می‌تواند از پوشه‌ای که کاربر مجاز کرده استفاده کند یا نه.

مقدار بازگشتی: یک boolean، همگام: true وقتی ساخت دسترسی به پوشه را روشن دارد. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید؛ بدون آن (و بدون Secure Keys) window.WebToApkFS به صفحه اضافه نمی‌شود؛ با فقط Secure Keys وجود دارد و پاسخ می‌دهد false.

window.WebToApkFS پیش از اجرای اولین اسکریپت صفحه‌ی شما اضافه می‌شود؛ در WebViewهای قدیمی‌تر از حدود 2021 فقط وقتی صفحه بارگذاری شد می‌رسد. برای پوشش آن‌ها هم، پیش از تصمیم‌گیری مدتی کوتاه منتظرش بمانید:

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

داخل لمس یک دکمه، صفحه مدت‌هاست بارگذاری شده، پس یک بررسی مستقیم کافی است:

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

دو بررسی را به یک وضعیت تبدیل کنید که کل صفحه‌ی پوشه را هدایت کند:

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

نکته‌ها: این چیزی درباره‌ی انتخاب‌شدن پوشه نمی‌گوید؛ برای آن از WebToApkFS.hasAccess() استفاده کنید. پیش از بارگذاری صفحه، window.WebToApk.isFolderAccessEnabled() خام (رشته‌ی "true"/"false") به همین سؤال پاسخ می‌دهد.

WebToApkFS.list کمک‌کننده#

WebToApkFS.list(relativePath)

نمونه

فایل‌ها و زیرپوشه‌های داخل یک پوشه از پوشه‌ی مجاز را فهرست می‌کند.

مقدار بازگشتی: یک آرایه، همگام: [{ name, uri, isDirectory, length, lastModified }] (length به بایت، lastModified به ms از 1970). پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید و یک پوشه‌ی مجاز داشته باشید.

یک مرورگر ساده‌ی فایل ('' خود پوشه‌ی مجاز است):

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

همه‌ی زیرپوشه‌ها را پیمایش کنید تا همه‌ی یادداشت‌های Markdown را جمع کنید (مسیرهایی که می‌توانید به 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';
}

جدیدترین پشتیبان و فضایی را که پشتیبان‌ها اشغال می‌کنند پیدا کنید:

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

نکته‌ها: پوشه‌ی خالی، پوشه‌ی گم‌شده، مسیری که فایل است، مسیر با .. و نبودن پوشه‌ی مجاز، همه [] برمی‌گردانند. فهرست مرتب نیست. هر uri با WebToApkFS.stat() هم کار می‌کند و WebToApkFS.readBytes().

WebToApkFS.mkdir کمک‌کننده#

WebToApkFS.mkdir(relativePath)

نمونه

یک پوشه داخل پوشه‌ی مجاز می‌سازد، از جمله هر پوشه‌ی والد گم‌شده.

مقدار بازگشتی: یک boolean، همگام: true وقتی پوشه پس از آن وجود دارد. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید و یک پوشه‌ی مجاز داشته باشید.

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

در اولین استفاده ساختار پوشه‌های یک پروژه را بسازید و هر پوشه‌ای را که ساخته نشد گزارش دهید:

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

دکمه‌ی «پوشه‌ی جدید» که وقتی نام قبلاً گرفته شده هشدار می‌دهد (چون mkdir برای پوشه‌ی موجود true جواب می‌دهد):

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

نکته‌ها: اگر پوشه از قبل وجود داشته باشد true برمی‌گرداند، پس فراخوانی آن پیش از هر ذخیره اشکالی ندارد. وقتی یک FILE با آن نام سر راه است، مسیر شامل .. است یا پوشه‌ای مجاز نیست، false برمی‌گرداند. WebToApkFS.writeText() خودش والدهای گم‌شده را می‌سازد، پس mkdir را فقط برای پوشه‌های خالی لازم دارید.

WebToApkFS.readText کمک‌کننده#

WebToApkFS.readText(relativePath)

نمونه

یک فایل متنی را از پوشه‌ی مجاز با UTF-8 می‌خواند.

مقدار بازگشتی: یک رشته، همگام: متن فایل، یا ''. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید و یک پوشه‌ی مجاز داشته باشید.

'' یعنی «فایل خالی» یا «چنین فایلی نیست». وقتی باید بدانید کدام است از WebToApkFS.stat() استفاده کنید:

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

یک یادداشت را در یک کادر متنی باز کنید:

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

یک CSV را که کاربر در پوشه انداخته وارد کنید:

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

نکته‌ها: کل فایل یک‌باره بارگذاری می‌شود؛ برای فایل‌های بزرگ یا باینری از WebToApkFS.readBytes() استفاده کنید. بایت‌هایی که UTF-8 معتبر نیستند به‌صورت نویسه‌ی جایگزین برمی‌گردند. یک پوشه، مسیر با .. یا نبودن پوشه‌ی مجاز هم برمی‌گرداند ''.

WebToApkFS.rename کمک‌کننده#

WebToApkFS.rename(relativePath, newName)

نمونه

یک فایل یا پوشه را داخل پوشه‌ی مجاز تغییر نام می‌دهد.

مقدار بازگشتی: یک boolean، همگام. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید و یک پوشه‌ی مجاز داشته باشید.

newName فقط نام جدید است، نه مسیر. ورودی در همان پوشه می‌ماند:

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

بدون تداخل تغییر نام دهید: "name (2)"، "name (3)" … را امتحان کنید تا نام آزاد پیدا شود:

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

امن ذخیره کنید: اول یک فایل موقت بنویسید، سپس جایگزینش کنید (تغییر نام نامِ گرفته‌شده را رد می‌کند، پس فایل قدیمی درست قبل از آن حذف می‌شود):

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

نکته‌ها: وقتی ورودی دیگری از قبل newName دارد، وقتی newName خالی است، ./.. است یا شامل / یا \ است، وقتی مسیر وجود ندارد یا پوشه‌ای مجاز نیست، false برمی‌گرداند. برای انتقال فایل بین پوشه‌ها، آن را بخوانید، در جای جدید بنویسید، سپس قدیمی را حذف کنید.

WebToApkFS.requestAccess کمک‌کننده#

WebToApkFS.requestAccess()

نمونه

انتخابگر پوشه‌ی سیستم را باز می‌کند تا کاربر یک پوشه به اپلیکیشن بدهد که بین راه‌اندازی‌های مجدد می‌ماند.

مقدار بازگشتی: یک Promise که همیشه حل می‌شود (هرگز رد نمی‌شود) با { success, folderUri, requestId }، به‌علاوه‌ی error وقتی success برابر false است. success یک boolean است؛ folderUri نشانی content://…/tree/… پوشه‌ی مجاز است، یا ''. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید.

از دکمه‌ای که کاربر می‌زند درخواست کنید و نشان دهید کدام پوشه متصل است:

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

فقط وقتی بپرسید که هنوز پوشه‌ای مجاز نشده است:

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

دکمه‌ی «تغییر پوشه» که هر راهی را که درخواست می‌تواند شکست بخورد توضیح می‌دهد:

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

نکته‌ها: آن را از یک لمس فراخوانی کنید، هرگز هنگام بارگذاری صفحه؛ انتخابگر اپلیکیشن را می‌پوشاند. success: false همراه error می‌آید: 'cancelled' (کاربر منصرف شد)، 'not_persisted' (Android مجوز را نگه نداشت)، 'disabled'، 'busy' (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، 'no_picker' یا 'foreign_origin'. انتخاب دوباره پوشه‌ی قبلی را جایگزین می‌کند.

WebToApkFS.writeText کمک‌کننده#

WebToApkFS.writeText(relativePath, content)

نمونه

یک فایل متنی UTF-8 را در پوشه‌ی مجاز می‌نویسد؛ آن را (و پوشه‌های والد گم‌شده را) می‌سازد یا جایگزین می‌کند.

مقدار بازگشتی: یک boolean، همگام: true وقتی فایل نوشته شد. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید و یک پوشه‌ی مجاز داشته باشید.

از یک دکمه ذخیره کنید:

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

یک شیء را به‌صورت JSON ذخیره کنید:

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

یک خط به گزارش روزانه اضافه کنید (افزودن وجود ندارد، پس بخوانید، اضافه کنید و دوباره بنویسید):

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

نکته‌ها: فایل موجود کاملاً جایگزین می‌شود؛ افزودن (append) وجود ندارد. content به رشته تبدیل می‌شود (null به '' تبدیل می‌شود). وقتی مسیر یک پوشه‌ی موجود باشد، شامل .. باشد یا پوشه‌ای مجاز نباشد، false برمی‌گرداند. نوع فایل از پسوند گرفته می‌شود، پس نام روی دیسک دقیقاً همان نامی است که داده‌اید.

clearFolderAccess پل#

window.WebToApk.clearFolderAccess()

مجوز ماندگار پوشه را آزاد می‌کند. برای صفحه‌ی خارجی رد می‌شود (بدون اثر).

نمونه

پوشه‌ی مجاز را فراموش می‌کند و مجوز را به Android پس می‌دهد.

مقدار بازگشتی: هیچ (همگام). WebToApkFS.clearAccess() همین کار را می‌کند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده باشد؛ با گزینه‌ی خاموش کاری نمی‌کند.

یک دکمه‌ی «قطع اتصال»:

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

هنگام خروج از حساب، پوشه را هم فراموش کنید تا نفر بعدی روی این گوشی پوشه‌ی خودش را انتخاب کند:

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

پوشه‌ای را که در برنامه‌ی Files جابه‌جا یا حذف شده فراموش کنید، سپس پیشنهاد انتخاب دوباره بدهید (اپلیکیشن وقتی پوشه‌ای برای اولین بار متصل می‌شود myapp-folder.json را می‌نویسد):

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

نکته‌ها: فایل‌های داخل پوشه حذف Nمی‌شوندN؛ اپلیکیشن فقط دیگر به آن‌ها دسترسی ندارد. بعد از این، hasFolderAccess() مقدار "false" می‌دهد و همه‌ی متدهای فایل شکست می‌خورند تا کاربر دوباره پوشه‌ای انتخاب کند.

deleteEntry پل#

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

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

یک فایل یا پوشه را داخل پوشه‌ی مجاز حذف می‌کند.

مقدار بازگشتی: رشته‌ی "true" یا "false"، همگام. WebToApkFS.delete(path, { recursive }) یک boolean واقعی برمی‌گرداند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده و یک پوشه‌ی مجاز.

با کمک‌تابع، بعد از پرسیدن از کاربر:

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

فراخوانی خام؛ آرگومان دوم رشته‌ی 'true' یا 'false':

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

فقط پنج پشتیبان جدیدتر را نگه دارید و بقیه را حذف کنید:

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

نکته‌ها: پوشه‌ای که خالی نیست فقط با recursive 'true' حذف می‌شود. خود پوشه‌ی مجاز ('') هرگز حذف نمی‌شود. حذف واقعی و فوری است: سطل زباله‌ای نیست.

getFolderAccessUri پل#

window.WebToApk.getFolderAccessUri(): String

نشانی درخت `content://` پوشه‌ی مجاز، یا "" (هیچ، غیرفعال، یا صفحه‌ی خارجی).

نمونه

نشانی پوشه‌ای را که کاربر مجاز کرده می‌دهد، تا بتوانید نشان دهید کدام پوشه متصل است.

مقدار بازگشتی: یک رشته، همگام: نشانی درخت مثل content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes، یا "" وقتی پوشه‌ای مجاز نیست یا گزینه خاموش است. WebToApkFS.getAccessUri() همین رشته را برمی‌گرداند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده باشد.

URI را به نامی تبدیل کنید که کاربر بشناسد:

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

فهرست فایل‌ها را فقط وقتی دوباره بارگذاری کنید که کاربر واقعاً پوشه‌ی دیگری انتخاب کرده است:

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

بگویید پوشه روی گوشی، روی کارت SD یا در یک اپلیکیشن ذخیره‌سازی دیگر است:

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

نکته‌ها: این URI را هرگز به متدهای فایل پس نمی‌دهید؛ همه‌ی آن‌ها مسیرهای نسبت به این پوشه می‌گیرند. نام را کنار دکمه‌ی «قطع اتصال» نشان دهید تا کاربر بداند فایل‌ها کجا می‌روند.

hasFolderAccess پل#

window.WebToApk.hasFolderAccess(): String

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

می‌گوید کاربر از قبل به اپلیکیشن پوشه‌ای داده یا نه.

مقدار بازگشتی: رشته‌ی "true" یا "false" (نه boolean)، همگام. WebToApkFS.hasAccess() یک boolean واقعی می‌دهد. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده باشد؛ بدون آن پاسخ همیشه "false".

متد خام از همان خط اول صفحه وجود دارد، پس برای بررسی در حین بارگذاری صفحه درست است (WebToApkFS فقط وقتی بارگذاری تمام شود اضافه می‌شود):

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

پیش از ذخیره بررسی کنید و انتخابگر را فقط وقتی باز کنید که هنوز پوشه‌ای متصل نیست:

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

یک تایمر ذخیره‌ی خودکار که فقط وقتی پوشه‌ای متصل است می‌نویسد و هرگز خودش انتخابگر را باز نمی‌کند:

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

نکته‌ها: با === 'true' مقایسه کنید؛ رشته‌ی "false" درست (truthy) است. این فقط می‌گوید اپلیکیشن پوشه‌ای را به خاطر دارد. اگر آن پوشه در برنامه‌ی Files حذف یا جابه‌جا شده باشد، listFolderEntries('') مقدار [] می‌دهد؛ از کاربر بخواهید دوباره انتخابش کند.

isFolderAccessEnabled پل#

window.WebToApk.isFolderAccessEnabled(): String

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

می‌گوید این اپلیکیشن اصلاً با روشن‌بودن دسترسی به پوشه ساخته شده یا نه.

مقدار بازگشتی: رشته‌ی "true" یا "false"، همگام. پیش‌نیاز: برای فراخوانی چیزی لازم نیست؛ فقط وقتی "true" جواب می‌دهد که Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده باشد.

از آن برای تصمیم‌گیری درباره‌ی نمایش قابلیت پوشه استفاده کنید. WebToApkFS.isAvailable() وقتی صفحه بارگذاری شد همین سؤال را به‌صورت boolean جواب می‌دهد:

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
}

یک ردیف تنظیمات را در سه حالتش اجرا کنید (در این ساخت نیست، متصل نیست، متصل است):

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

صفحه را پیش از ترسیم علامت بزنید تا CSS بتواند کنترل‌های مخصوص پوشه را بدون پرش نشان دهد:

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

نکته‌ها: این چیزی درباره‌ی انتخاب‌شدن پوشه نمی‌گوید؛ برای آن از hasFolderAccess() استفاده کنید. در مرورگر window.WebToApk وجود ندارد، پس بررسی بالا بدون خطا false جواب می‌دهد.

listFolderEntries پل#

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

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

فایل‌ها و زیرپوشه‌های داخل یک پوشه از پوشه‌ی مجاز را فهرست می‌کند.

مقدار بازگشتی: یک رشته‌ی JSON، همگام: آرایه‌ای از { name, uri, isDirectory, length, lastModified } (length به بایت، lastModified به ms از 1970). WebToApkFS.list(path) آن را برایتان تجزیه می‌کند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده و یک پوشه‌ی مجاز.

با کمک‌تابع ('' خود پوشه‌ی مجاز است):

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

فراخوانی خام که متنی برمی‌گرداند که باید تجزیه کنید:

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

جدیدترین فایل پشتیبان را پیدا کنید و نشان دهید کی ساخته شده:

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

نکته‌ها: پوشه‌ی خالی، پوشه‌ی گم‌شده، مسیری که فایل است، مسیر با ..، نبودن پوشه‌ی مجاز و صفحه‌ای از وب‌سایت خارجی، همه [] جواب می‌دهند؛ اگر لازم است «بدون پوشه» را تشخیص دهید اول hasFolderAccess() را فراخوانی کنید. مسیرها نسبی هستند، از / استفاده می‌کنند و هرگز از پوشه‌ی مجاز بالاتر نمی‌روند.

mkdir پل#

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

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

یک پوشه داخل پوشه‌ی مجاز می‌سازد، از جمله هر پوشه‌ی والد گم‌شده.

مقدار بازگشتی: رشته‌ی "true" یا "false"، همگام. WebToApkFS.mkdir(path) یک boolean واقعی برمی‌گرداند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده و یک پوشه‌ی مجاز.

با کمک‌تابع:

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

فراخوانی خام:

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

برای یک خروجی، پوشه‌ای با تاریخ بسازید و همه‌ی آیتم‌ها را در آن بنویسید:

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

نکته‌ها: وقتی پوشه از قبل وجود دارد "true" جواب می‌دهد، پس فراخوانی آن پیش از هر ذخیره امن است. وقتی یک FILE با آن نام سر راه است، مسیر شامل .. است یا پوشه‌ای مجاز نیست، "false" جواب می‌دهد.

readTextFile پل#

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

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

یک فایل متنی را از پوشه‌ی مجاز با UTF-8 می‌خواند.

مقدار بازگشتی: یک رشته، همگام: متن فایل، یا "". WebToApkFS.readText(path) همین را برمی‌گرداند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده و یک پوشه‌ی مجاز.

"" یعنی «فایل خالی» یا «چنین فایلی نیست»، پس وقتی تفاوت مهم است با WebToApkFS.stat() بررسی کنید:

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;

فراخوانی خام:

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

همه‌ی یادداشت‌های .txt و .md را از یک پوشه وارد کنید:

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

نکته‌ها: کل فایل در حافظه خوانده می‌شود؛ برای یادداشت و JSON خوب است، برای فایل‌های بزرگ نه؛ برای آن‌ها از WebToApkFS.readBytes استفاده کنید. بایت‌هایی که UTF-8 معتبر نیستند به‌صورت نویسه‌ی جایگزین برمی‌گردند. مسیر یک پوشه، مسیر با ..، نبودن پوشه‌ی مجاز یا صفحه‌ای از وب‌سایت خارجی، همه پاسخ می‌دهند "".

renameEntry پل#

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

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

یک فایل یا پوشه را داخل پوشه‌ی مجاز تغییر نام می‌دهد.

مقدار بازگشتی: رشته‌ی "true" یا "false"، همگام. WebToApkFS.rename(path, newName) یک boolean واقعی برمی‌گرداند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده و یک پوشه‌ی مجاز.

newName یک نام ساده است، نه مسیر؛ ورودی در همان پوشه می‌ماند:

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

فراخوانی خام:

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

پشتیبان قبلی را با تغییر نام نگه دارید، پیش از نوشتن پشتیبان جدید:

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

نکته‌ها: وقتی ورودی دیگری از قبل newName دارد، وقتی newName خالی است یا شامل / یا \ است، وقتی مسیر وجود ندارد یا پوشه‌ای مجاز نیست شکست می‌خورد. برای انتقال فایل به پوشه‌ی دیگر، آن را آنجا بنویسید و قدیمی را حذف کنید.

requestFolderAccess پل#

window.WebToApk.requestFolderAccess(requestId: String)

انتخابگر پوشه‌ی سیستم را باز می‌کند و دسترسی خواندن/نوشتن به پوشه‌ی انتخاب‌شده را ماندگار می‌کند. پاسخ روی `WebToApkOnFolderAccessResult` می‌رسد؛ هم‌زمان فقط یک انتخابگر می‌تواند باز باشد، پس درخواستی که هنگام باز بودن یکی داده شود فوراً با `error:"busy"` پاسخ می‌گیرد و انتخابگر باز پاسخ خودش را نگه می‌دارد.

نمونه

انتخابگر پوشه‌ی سیستم را باز می‌کند تا کاربر یک پوشه به اپلیکیشن بدهد. صفحه‌ها معمولاً از WebToApkFS.requestAccess() استفاده می‌کنند که این فراخوانی را در یک Promise می‌پیچد.

مقدار بازگشتی: هیچ. پاسخ بعداً از طریق یک callback داخلی که WebToApkFS مالک آن است برمی‌گردد؛ WebToApkFS.requestAccess() با { success, folderUri, requestId } حل می‌شود، به‌علاوه‌ی error وقتی success برابر false است. پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه Native Folder Access (SAF) را تیک بزنید.

از لمس یک دکمه درخواست کنید، با کمک‌تابع:

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

فقط بار اول بپرسید، سپس زیرپوشه‌ی خود اپلیکیشن را بسازید:

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

دقیقاً به کاربر بگویید چرا پوشه‌ای متصل نشد، با کدهای واقعی error:

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

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

نکته‌ها: متد خام را خودتان فراخوانی نکنید: تنها پاسخش callbackی است که کمک‌تابع به آن گوش می‌دهد. success برابر false است وقتی کاربر منصرف شود (error:'cancelled')، وقتی Android مجوز را نگه ندارد ('not_persisted')، و فوراً وقتی ساخت گزینه را روشن نکرده ('disabled') یا صفحه‌ی روی صفحه از خود اپلیکیشن نیست ('foreign_origin'). مجوز بین راه‌اندازی‌های مجدد اپلیکیشن می‌ماند؛ انتخاب دوباره پوشه را جایگزین می‌کند. هم‌زمان فقط یک انتخابگر می‌تواند باز باشد؛ درخواستی که هنگام باز بودن یکی داده شود فوراً با error:'busy' پاسخ می‌گیرد و انتخابگر باز پاسخ خودش را نگه می‌دارد.

writeTextFile پل#

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

دسترسی به پوشه‌ی امن برای Play با ACTION_OPEN_DOCUMENT_TREE و مجوزهای URI ماندگار. استفاده در جاوااسکریپت: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." یا "" window.WebToApk.clearFolderAccess() // دسترسی ماندگار را آزاد می‌کند Callback از Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (آرگومان چهارم، در موفقیت "") می‌گوید چرا درخواست شکست خورد: "disabled"، "foreign_origin"، "busy" (انتخابگری برای درخواست قبلی باز است؛ آن هنوز پاسخ خودش را می‌گیرد)، "cancelled"، "not_persisted"، "no_picker".

به‌جای توضیح جداگانه، در گروهش توضیح داده شده است: Native Folder Access (SAF) Bridge.

نمونه

یک فایل متنی (UTF-8) را در پوشه‌ی مجاز می‌نویسد؛ آن را می‌سازد یا محتوایش را جایگزین می‌کند.

مقدار بازگشتی: رشته‌ی "true" یا "false"، همگام. WebToApkFS.writeText(path, text) یک boolean واقعی برمی‌گرداند. پیش‌نیاز: Native Folder Access (SAF) در مرحله 4 (Access) تیک خورده و یک پوشه‌ی مجاز.

با کمک‌تابع:

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

فراخوانی خام که پاسخش متن است:

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

یک خط به گزارش روزانه اضافه کنید؛ نوشتن فایل را جایگزین می‌کند، پس اول آن را بخوانید:

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

نکته‌ها: فایل موجود کاملاً جایگزین می‌شود. وقتی مسیر یک پوشه‌ی موجود باشد، شامل .. باشد یا پوشه‌ای مجاز نباشد شکست می‌خورد. نوع فایل از پسوند (.json، .txt، .md…) گرفته می‌شود، پس نامی که می‌دهید همان نام روی دیسک است.