ساخت APK رایگان

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

فایل‌ها: هر بار یک فایل

انتخابگر سیستم و خواندن/نوشتن دودویی تکه‌تکه، برای فایل‌هایی که برای نگه داشتن در یک رشته بیش از حد بزرگ‌اند.

AppMint.downloadFile کمک‌کننده#

AppMint.downloadFile(data, fileName, mimeType?)

نمونه

فایلی را که صفحهٔ شما ساخته - از یک رشتهٔ base64، یک نشانی data:، یک نشانی blob: یا یک Blob - با نام فایلی که انتخاب می‌کنید ذخیره می‌کند. برگهٔ «ذخیره به‌عنوان…» در Android با همان نام پر‌شده باز می‌شود.

مقدار بازگشتی: (همگام) true وقتی ذخیره به Android سپرده شد (false فقط برای نشانی data: که نتوانسته تجزیه کند). اگر data Blob یا رشته نباشد TypeError پرتاب می‌شود. کاربر همچنان پوشه را انتخاب می‌کند و می‌تواند لغو کند. پیش‌نیاز: هیچ؛ همیشه در اپلیکیشن موجود است.

رشتهٔ Base64 به‌همراه نوع MIME:

function exportCsv(rows) {
  const csv = rows.map(function (r) { return r.join(','); }).join('\n');
  const b64 = btoa(unescape(encodeURIComponent(csv)));          // UTF-8 safe
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(b64, 'Ledger_Q3.csv', 'text/csv');
  } else {
    const a = document.createElement('a');                       // a normal browser
    a.href = 'data:text/csv;base64,' + b64;
    a.download = 'Ledger_Q3.csv';
    a.click();
  }
}

یک نشانی data: (نوع MIME از نشانی می‌آید) یا یک Blob:

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

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

یک PDF با کدگذاری base64 که سرور شما در JSON برمی‌گرداند؛ اگر نام پسوند نداشته باشد، .pdf از روی نوع MIME به آن اضافه می‌شود:

async function downloadInvoice(id) {
  const res = await fetch('/api/invoices/' + id + '?format=base64');
  const body = await res.json();                                  // { base64: 'JVBERi0x…' }
  if (!window.AppMint || typeof AppMint.downloadFile !== 'function') {
    location.href = 'data:application/pdf;base64,' + body.base64;
    return;
  }
  AppMint.downloadFile(body.base64, 'Invoice_' + id, 'application/pdf');   // saved as Invoice_<id>.pdf
}

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

// earlier: preview.src = URL.createObjectURL(editedImageBlob);
document.getElementById('save-photo').addEventListener('click', function () {
  const preview = document.getElementById('preview');
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(preview.src, 'Edited_Photo.jpg');
  }
});

نکته‌ها: فقط آخرین بخش fileName استفاده می‌شود (بدون پوشه). به نامی که پسوند ندارد، از روی نوع MIME یک پسوند داده می‌شود. آرگومان سوم فقط برای رشتهٔ base64 ساده استفاده می‌شود. اگر گوشی اصلاً صفحهٔ «ذخیره به‌عنوان…» نداشته باشد، فایل مستقیم در Downloads نوشته می‌شود. کد معمولیِ <a download> شما هم کار می‌کند؛ ببینید downloads.

AppMint.saveBlob کمک‌کننده#

AppMint.saveBlob(blob, fileName)

نمونه

یک Blob (یا File) را با نامی که انتخاب می‌کنید ذخیره می‌کند. برگهٔ «ذخیره به‌عنوان…» در Android با همان نام پر‌شده باز می‌شود.

مقدار بازگشتی: (همگام) true وقتی ذخیره به Android سپرده شد. اگر آرگومان اول Blob نباشد TypeError پرتاب می‌شود. پیش‌نیاز: هیچ؛ همیشه در اپلیکیشن موجود است.

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

یک File نام خودش را می‌آورد، پس می‌توان نام را حذف کرد:

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

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

async function saveStatement(month, token) {
  const res = await fetch('/api/statements/' + month, {
    headers: { Authorization: 'Bearer ' + token }
  });
  if (!res.ok) { alert('Could not fetch the statement (' + res.status + ')'); return; }
  const blob = await res.blob();
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Statement_' + month + '.pdf');
  }
}

چند فایل در یک ZIP بسته‌بندی شده (اینجا با JSZip) و یک‌جا ذخیره شود:

async function exportAll(notes) {
  const zip = new JSZip();
  notes.forEach(function (n) { zip.file(n.title + '.txt', n.body); });
  zip.file('index.json', JSON.stringify(notes.map(function (n) { return n.title; })));
  const blob = await zip.generateAsync({ type: 'blob' });
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Notes_' + new Date().toISOString().slice(0, 10) + '.zip');
  }
}

نکته‌ها: Blob در حافظه خوانده و یک‌جا به Android فرستاده می‌شود، پس Blobهای خیلی بزرگ (صدها MB) ممکن است حافظه کم بیاورند. کاربر پوشه را انتخاب می‌کند و ممکن است منصرف شود؛ چیزی به صفحه نمی‌گوید کدام اتفاق افتاد.

AppMint.setDownloadName کمک‌کننده#

AppMint.setDownloadName(url, fileName)

نمونه

به یک نشانی blob: یا data: نام فایلی را که باید با آن ذخیره شود می‌دهد، پیش از آنکه کد شما (یا یک کتابخانه) دانلود را شروع کند.

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

وقتی کتابخانه‌ای نشانیی را که قبلاً ساخته‌اید دانلود می‌کند و نمی‌توانید به آن نام بدهید، مفید است:

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

یک PDF که صفحهٔ شما با window.open باز می‌کند به نمایشگر PDF گوشی می‌رود؛ ابتدا به آن نام بدهید تا «download.pdf» نباشد:

function previewPdf(pdfBlob, invoiceNo) {
  const url = URL.createObjectURL(pdfBlob);
  if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
    AppMint.setDownloadName(url, 'Invoice_' + invoiceNo + '.pdf');
  }
  window.open(url, '_blank');
}

یک نشانی data: هم به همین شکل کار می‌کند، مثلاً تصویر نموداری که کتابخانه‌ای بدون ویژگی download به آن پیوند می‌دهد:

const dataUrl = chart.toBase64Image();         // e.g. Chart.js: 'data:image/png;base64,…'
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
  AppMint.setDownloadName(dataUrl, 'Sales_by_month.png');
}
const link = document.getElementById('chart-link');
link.href = dataUrl;                           // a tap on this link saves Sales_by_month.png
link.textContent = 'Save chart';

نکته‌ها: نام حتی پس از URL.revokeObjectURL(url) به خاطر سپرده می‌شود، چون بعضی کتابخانه‌ها پیش از پایان ذخیره آن را باطل می‌کنند. به ندرت به این نیاز دارید: <a download="name">، URL.createObjectURL(file) (یک File نام دارد) و saveAs(blob, name) در FileSaver همه به‌صورت خودکار شناسایی می‌شوند. نام 'download' به‌عنوان «بدون نام» نادیده گرفته می‌شود.

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

WebToApkFS.maxChunkSize()

نمونه

به شما می‌گوید هر فراخوانی readBytes یا writeBytes حداکثر چند بایت را جابه‌جا می‌کند (4 مگابایت) تا بتوانید حلقهٔ تکه‌ها را متناسب بنویسید.

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

function copyFile(fromUri, toPath) {
  if (!window.WebToApkFS) return { ok: false, error: 'not in the app' };
  const step = WebToApkFS.maxChunkSize() || 1048576;   // fall back to 1 MB pieces
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    if (r.bytesRead > 0) {
      const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
      if (!w.ok) return w;
    }
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}

پیشرفت خواندن را نشان دهید: اندازه‌ی تکه نشان می‌دهد خواندن یک فایل چند مرحله می‌شود:

function readWithProgress(uri, onProgress) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return info;
  const step = WebToApkFS.maxChunkSize() || 1048576;
  const steps = Math.max(1, Math.ceil(info.size / step));
  for (let i = 0; i < steps; i++) {
    const r = WebToApkFS.readBytes(uri, i * step, step);
    if (!r.ok) return r;
    onProgress(Math.round(((i + 1) / steps) * 100), r.base64);
  }
  return { ok: true, size: info.size };
}

یک آرایهٔ بایتیِ بزرگ را به یک فایل در پوشهٔ مجاز اضافه کنید، هر بار یک بخشِ مجاز:

function appendBytes(path, bytes) {                    // bytes: a Uint8Array
  const step = WebToApkFS.maxChunkSize();
  if (!step) return { ok: false, error: 'not in the app' };
  for (let at = 0; at < bytes.length; at += step) {
    const piece = bytes.subarray(at, at + step);
    let bin = '';
    for (let i = 0; i < piece.length; i += 0x8000) bin += String.fromCharCode.apply(null, piece.subarray(i, i + 0x8000));
    const w = WebToApkFS.writeBytes(path, btoa(bin), 0, 'append');   // 'append' ignores the offset
    if (!w.ok) return w;
  }
  return { ok: true };
}

نکته‌ها: تکهٔ کوچک‌تر صفحه را پاسخگوتر نگه می‌دارد (هر فراخوانی همگام است)؛ سقف فقط می‌گوید یک فراخوانی چقدر می‌تواند بزرگ باشد.

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

WebToApkFS.pickFile(mimeFilter)

نمونه

انتخابگر فایل Android را باز می‌کند تا کاربر یک فایل انتخاب کند؛ آن‌گاه صفحهٔ شما می‌تواند بایت‌های واقعی آن فایل را بخواند و بنویسد، حتی پس از راه‌اندازی مجدد اپلیکیشن.

مقدار بازگشتی: Promise<{ok, uri, name, size, mime, error}>. در موفقیت ok برابر true است و uri یک نشانی content:// است که به stat، readBytes، writeBytes می‌دهید. در شکست ok برابر false است و error یکی از 'cancelled'، 'disabled'، 'no-picker'، 'busy' (انتخابگری از قبل برای فراخوانی قبلی باز است که پاسخ خودش را نگه می‌دارد)، 'foreign_origin' یا 'unsupported' (بدون اپلیکیشن). پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

async function openOneFile() {
  if (!window.WebToApkFS) { alert('Open this page in the app to pick a file.'); return; }
  const f = await WebToApkFS.pickFile('application/pdf');   // '' = any file, 'image/*' = pictures
  if (!f.ok) {
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  console.log(f.name, f.size + ' bytes', f.mime);
  localStorage.setItem('lastFile', f.uri);   // the grant survives a restart
}

uri ذخیره‌شده را بعداً دوباره استفاده کنید، بدون اینکه بار دوم از کاربر بپرسید:

const uri = localStorage.getItem('lastFile');
if (uri && window.WebToApkFS) {
  const info = WebToApkFS.stat(uri);
  if (info.ok) console.log('still here:', info.name);
  else console.log('file is gone or access was removed:', info.error);
}

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

async function importTable() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');        // CSV and TSV files are both text/*
  if (!f.ok) {
    if (f.error === 'busy') return;                     // a picker is already open
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  if (!/\.(csv|tsv)$/i.test(f.name)) {
    WebToApkFS.releaseFile(f.uri);
    alert(f.name + ' is not a CSV or TSV file.');
    return;
  }
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) showTable(atob(r.base64), f.name.toLowerCase().endsWith('.tsv') ? '\t' : ',');
  WebToApkFS.releaseFile(f.uri);
}

نکته‌ها: فیلتر یک الگوی نوع MIME است ('image/*'، 'text/csv')، نه فهرست. انتخابگر دسترسی خواندن و نوشتن می‌خواهد و آن را نگه می‌دارد. Android تعداد فایل‌هایی را که یک اپلیکیشن می‌تواند به این شکل نگه دارد محدود می‌کند، پس وقتی کارتان با فایل تمام شد WebToApkFS.releaseFile(uri) را صدا بزنید. برای یک پوشهٔ کامل به‌جای آن از WebToApkFS.requestAccess() استفاده کنید. WebToApkFS وقتی Native Folder Access یا Secure Keys علامت خورده باشد نصب می‌شود؛ فراخوانی‌هایش فقط با Native Folder Access کار می‌کنند (WebToApkFS.isAvailable()).

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

WebToApkFS.readAll(pathOrUri)

نمونه

کل یک فایل را در یک رشتهٔ base64 می‌خواند. حلقهٔ readBytes را برای شما، هر بار 4 مگابایت، اجرا می‌کند.

مقدار بازگشتی: (همگام) {ok:true, base64, size}، یا {ok:false, error} مربوط به اولین تکهٔ ناموفق. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

یک تصویر انتخاب‌شده را در یک <img>:

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

یک فایل متنی UTF-8 را بخوانید (برای هر زبانی کار می‌کند، نه فقط انگلیسی):

function readUtf8(pathOrUri) {
  const r = WebToApkFS.readAll(pathOrUri);
  if (!r.ok) throw new Error(r.error);
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  return new TextDecoder('utf-8').decode(bytes);
}

چک‌سام SHA-256 یک فایل را نشان دهید، مثلاً برای مقایسهٔ یک دانلود با نسخهٔ منتشرشده:

async function checksum() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
  const hex = Array.from(hash, function (b) { return b.toString(16).padStart(2, '0'); }).join('');
  console.log(f.name, r.size + ' bytes', hex);
}

نکته‌ها: کل فایل در حافظه می‌ماند (ابتدا به‌صورت بایت و سپس base64) و فراخوانی در حین اجرا صفحه را مسدود می‌کند. برای فایل‌هایی که به‌راحتی در حافظه جا می‌شوند استفاده کنید؛ برای فایل‌های بزرگ روی WebToApkFS.readBytes حلقه بزنید و هر تکه را پردازش کنید. برای متن ساده داخل پوشهٔ مجاز، WebToApkFS.readText(path) ساده‌تر است.

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

WebToApkFS.readBytes(pathOrUri, offset, length)

نمونه

بایت‌های واقعی یک فایل را، از یک موقعیت بایتی، به‌صورت base64 می‌خواند. برای تصویر، PDF و ZIP ایمن است - هیچ چیز با رمزگشایی متن تغییر نمی‌کند.

مقدار بازگشتی: (همگام) {ok:true, base64, bytesRead, offset, size, eof} یا {ok:false, error} ('not-found'، 'disabled'، 'cannot-open'، 'offset past end of file (N bytes)'، 'foreign_origin'، 'unsupported'). پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

64 کیلوبایت اول را بخوانید، مثلاً برای بررسی سرآیند فایل:

async function checkPng() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('image/*');
  if (!f.ok) return;
  const r = WebToApkFS.readBytes(f.uri, 0, 65536);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const head = atob(r.base64).slice(1, 4);
  console.log(head === 'PNG' ? 'a PNG' : 'not a PNG', '- file is', r.size, 'bytes');
}

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

function eachChunk(uri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(uri, offset, step);
    if (!r.ok) return r;                 // stop on the first error
    onChunk(r.base64, offset);
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}

فقط انتهای یک فایل را بخوانید، مثلاً برچسب 128 بایتیِ پایان یک MP3:

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

نکته‌ها: یک فراخوانی حداکثر WebToApkFS.maxChunkSize() بایت (4 مگابایت) می‌خواند؛ length برابر 0 یعنی «تا هر اندازه که یک فراخوانی اجازه می‌دهد». روی bytesRead حلقه بزنید و روی eof متوقف شوید. فراخوانی همگام است، پس تکه‌ها را روی رشتهٔ UI متوسط نگه دارید. برای فایل‌های کوچک WebToApkFS.readAll(uri) حلقه را برای شما انجام می‌دهد.

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

WebToApkFS.readSealed(pathOrUri, alias)

نمونه

فایلی را که WebToApkFS.writeSealed نوشته می‌خواند و با همان کلید keystore رمزگشایی می‌کند.

مقدار بازگشتی: (همگام) {ok:true, base64} (بایت‌های اصلی)، یا {ok:false, error} - 'keystore-disabled'، 'corrupt-sealed-file'، یک کد keystore ('no-such-key' بعد از حذف کلید، 'auth-required'، 'key-invalidated') یا یک خطای فایل ('not-found' …). پیش‌نیاز: هنگام ساخت، در مرحله 4 (Access) گزینه‌های Native Folder Access (SAF) و Secure Keys (encryption) را تیک بزنید.

function loadSecret() {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return null;
  const out = WebToApkFS.readSealed('secret.bin', 'vault');
  if (out.ok) {
    const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
    return new TextDecoder().decode(bytes);
  }
  if (out.error === 'auth-required') {
    alert('Unlock your phone, then try again.');
  } else if (out.error === 'no-such-key') {
    alert('This note was deleted.');
  } else {
    alert('Could not open the note: ' + out.error);
  }
  return null;
}

تنظیمات اپلیکیشن را در یک فایل JSON مهر و موم‌شده نگه دارید، با پیش‌فرض‌ها پیش از اولین ذخیره:

function loadSettings() {
  const defaults = { theme: 'light', pinHint: '' };
  const out = WebToApkFS.readSealed('settings.sealed', 'settings');
  if (!out.ok) {
    if (out.error === 'not-found') return defaults;      // nothing saved yet
    throw new Error('Settings could not be opened: ' + out.error);
  }
  const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
  return Object.assign(defaults, JSON.parse(new TextDecoder().decode(bytes)));
}

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

function rekey(path, oldAlias, newAlias) {
  const plain = WebToApkFS.readSealed(path, oldAlias);
  if (!plain.ok) return plain;
  if (!AppMintKeystore.hasKey(newAlias)) {
    const k = AppMintKeystore.generateKey(newAlias, {});
    if (!k.ok) return k;
  }
  const w = WebToApkFS.writeSealed(path, plain.base64, newAlias);
  if (!w.ok) return w;
  AppMintKeystore.deleteKey(oldAlias);                  // only after the new copy is written
  return { ok: true };
}

نکته‌ها: بعد از اجرای AppMintKeystore.deleteKey(alias) برای همیشه شکست می‌خورد؛ هدف همین است. کل فایل در حافظه و همگام خوانده و رمزگشایی می‌شود؛ فایل‌های مهروموم‌شده را کوچک نگه دارید (یادداشت‌ها، کلیدها، توکن‌ها).

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

WebToApkFS.releaseFile(uri)

نمونه

دسترسی بلندمدتی را که اپلیکیشن شما وقتی کاربر فایلی را با آن انتخاب کرد گرفت، پس می‌دهد WebToApkFS.pickFile.

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

async function importOnce() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/csv');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) importCsv(atob(r.base64));
  WebToApkFS.releaseFile(f.uri);       // done with it: do not keep the grant
}

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

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

دکمهٔ «فراموش کردن فایل‌های اخیر» که همهٔ فایل‌هایی را که اپلیکیشن به خاطر سپرده آزاد می‌کند:

function forgetRecentFiles() {
  const recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  let released = 0;
  for (const uri of recent) {
    if (WebToApkFS.releaseFile(uri)) released++;       // false: already gone or never granted
  }
  localStorage.removeItem('recentFiles');
  console.log('released', released, 'of', recent.length);
}

نکته‌ها: Android تعداد فایل‌های انتخاب‌شده‌ای را که یک اپلیکیشن می‌تواند به آن‌ها دسترسی نگه دارد محدود می‌کند. اپلیکیشنی که فایل‌های زیادی انتخاب می‌کند (واردکننده، نمایشگر) باید هر فایل را پس از اتمام آزاد کند. پس از آزادسازی، stat / readBytes / writeBytes روی آن نشانی not-found پاسخ می‌دهند. فایل را حذف نمی‌کند.

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

WebToApkFS.stat(pathOrUri)

نمونه

نام، اندازه، نوع و تاریخ یک فایل را بدون خواندن محتوایش به شما می‌گوید.

مقدار بازگشتی: (همگام) {ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}، یا {ok:false, error} که در آن error = 'not-found'، 'disabled'، 'foreign_origin' یا 'unsupported' است. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

async function showInfo() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const info = WebToApkFS.stat(f.uri);
  if (!info.ok) { alert('Cannot read file info: ' + info.error); return; }
  console.log(info.name, info.size + ' bytes', info.mime,
              new Date(info.lastModified).toLocaleString(),
              info.canWrite ? 'writable' : 'read only');
}

مسیری داخل پوشه‌ای که کاربر با WebToApkFS.requestAccess() مجاز کرده هم کار می‌کند:

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

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

function importIfChanged(uri) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return 'gone: ' + info.error;
  const seen = JSON.parse(localStorage.getItem('lastImport') || '{}');
  if (seen.size === info.size && seen.lastModified === info.lastModified) return 'unchanged';
  const r = WebToApkFS.readAll(uri);
  if (!r.ok) return 'read failed: ' + r.error;
  importData(atob(r.base64));
  localStorage.setItem('lastImport', JSON.stringify({ size: info.size, lastModified: info.lastModified }));
  return 'imported';
}

نکته‌ها: pathOrUri یک نشانی content:// از pickFile() یا مسیری نسبت به پوشهٔ مجاز است. نشانیی که هرگز به اپلیکیشن شما داده نشده not-found برمی‌گرداند. lastModified میلی‌ثانیه از سال 1970 است (اگر ارائه‌دهنده نگوید، 0).

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

WebToApkFS.writeAll(pathOrUri, base64)

نمونه

کل محتوای یک فایل را با بایت‌های شما جایگزین می‌کند. داده را برای شما به بخش‌های 4 مگابایتی تقسیم می‌کند: بخش اول فایل را کوتاه می‌کند، بقیه بعد از آن وصله می‌شوند.

مقدار بازگشتی: (همگام) {ok:true, bytesWritten}، یا {ok:false, error} مربوط به اولین بخشِ ناموفق. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

متن UTF-8 (هر زبانی) را در فایلی که کاربر انتخاب کرده ذخیره کنید:

function bytesToBase64(bytes) {
  let s = '';
  for (let i = 0; i < bytes.length; i += 0x8000) {
    s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  }
  return btoa(s);
}

async function saveNote(text) {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/plain');
  if (!f.ok) return;
  const r = WebToApkFS.writeAll(f.uri, bytesToBase64(new TextEncoder().encode(text)));
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes');
  else alert('Could not save: ' + r.error);
}

یک تصویر canvas را در پوشهٔ مجاز بنویسید (اگر فایل نباشد ساخته می‌شود):

const b64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
const r = WebToApkFS.writeAll('drawings/sketch.png', b64);
if (!r.ok) alert('Could not save: ' + r.error);

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

async function backup(data) {
  if (!window.WebToApkFS || !WebToApkFS.isAvailable()) return;
  if (!WebToApkFS.hasAccess()) {
    const a = await WebToApkFS.requestAccess();
    if (!a.success) return;
  }
  const bytes = new TextEncoder().encode(JSON.stringify(data));
  let bin = '';
  for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  const r = WebToApkFS.writeAll('backup.json', btoa(bin));
  if (!r.ok) { alert('Backup failed: ' + r.error); return; }
  const info = WebToApkFS.stat('backup.json');
  console.log('backup is', info.ok ? info.size : '?', 'bytes');
}

نکته‌ها: کل ورودی هنگام نوشتن در حافظه می‌ماند و فراخوانی صفحه را مسدود می‌کند. اگر یک بخشِ بعدی ناموفق شود، فایل از قبل بخش‌های قبلی را دارد؛ ok را بررسی کنید و به کاربر بگویید. برای محرمانه نگه داشتن فایل، به‌جای آن از WebToApkFS.writeSealed استفاده کنید.

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

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

نمونه

بایت‌های واقعی را در یک فایل می‌نویسد و به شما می‌گوید چند بایت رسیده است. می‌تواند بایت‌های وسط را وصله کند، کل فایل را جایگزین کند یا به انتها اضافه کند.

مقدار بازگشتی: (همگام) {ok, bytesWritten, offset, size} - size پس از تخلیهٔ داده روی دیسک از فایل دوباره خوانده می‌شود - یا {ok:false, error}. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

Patch - 4 بایت را در موقعیت 100 تغییر دهید و بقیهٔ فایل را نگه دارید:

const r = WebToApkFS.writeBytes(fileUri, btoa('ABCD'), 100, 'patch');
if (r.ok) console.log('file is now', r.size, 'bytes');
else alert('Write failed: ' + r.error);

Truncate - کل فایل را جایگزین کنید (آفست نادیده گرفته می‌شود):

const csv = 'name,score\nAsha,12\n';
const r = WebToApkFS.writeBytes('exports/scores.csv', btoa(csv), 0, 'truncate');
if (!r.ok) alert('Could not save: ' + r.error);

Append - به انتها اضافه کنید (آفست نادیده گرفته می‌شود):

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

نکته‌ها: mode برابر 'patch' (پیش‌فرض)، 'truncate' یا 'append' است. حداکثر 4 مگابایت در هر فراخوانی (WebToApkFS.maxChunkSize())؛ برای داده‌های بزرگ‌تر از WebToApkFS.writeAll استفاده کنید. مسیری داخل پوشهٔ مجاز اگر وجود نداشته باشد ساخته می‌شود؛ یک نشانی content:// انتخاب‌شده باید همانی باشد که کاربر انتخاب کرده. btoa فقط متن Latin-1 را می‌پذیرد - برای متن‌های دیگر ابتدا به بایت کدگذاری کنید (ببینید WebToApkFS.writeAll). خطاها: disabled، foreign_origin (صفحهٔ روی صفحه از خودِ اپلیکیشن نیست)، bad mode: x, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

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

WebToApkFS.writeSealed(pathOrUri, base64, alias)

نمونه

یک فایل را با کلیدی مبتنی بر سخت‌افزار از AppMintKeystore رمزگذاری‌شده می‌نویسد. حذف آن کلید در آینده فایل را برای همه ناخواناپذیر می‌کند - تنها «حذف امن» واقعی روی گوشی.

مقدار بازگشتی: (همگام) {ok:true, bytesWritten} (اندازهٔ رمزگذاری‌شده روی دیسک)، یا {ok:false, error} - 'keystore-disabled'، یک کد keystore مانند 'no-such-key'، 'auth-required'، 'key-invalidated'، یا یک خطای فایل. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینه‌های Native Folder Access (SAF) و Secure Keys (encryption) را علامت بزنید.

function saveSecret(text) {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) {
    alert('Secure storage is not available in this build.');
    return false;
  }
  if (!AppMintKeystore.hasKey('vault')) {
    const k = AppMintKeystore.generateKey('vault', {});
    if (!k.ok) { alert('Could not create key: ' + k.error); return false; }
  }
  const bytes = new TextEncoder().encode(text);
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  const r = WebToApkFS.writeSealed('secret.bin', btoa(bin), 'vault');
  if (!r.ok) alert('Could not save: ' + r.error);
  return r.ok;
}

آن را واقعاً حذف کنید - کلید را نابود کنید، سپس فایل را بردارید:

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

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

async function sealPhoto(file) {                  // a File from <input type="file" accept="image/*">
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return false;
  if (!WebToApkFS.hasAccess()) {
    const g = await WebToApkFS.requestAccess();
    if (!g.success) return false;
  }
  if (!AppMintKeystore.hasKey('vault')) AppMintKeystore.generateKey('vault', {});
  const dataUrl = await new Promise(function (resolve, reject) {
    const fr = new FileReader();
    fr.onload = function () { resolve(fr.result); };
    fr.onerror = reject;
    fr.readAsDataURL(file);
  });
  const b64 = dataUrl.slice(dataUrl.indexOf(',') + 1);
  const r = WebToApkFS.writeSealed('photo-' + Date.now() + '.sealed', b64, 'vault');
  if (!r.ok) alert('Not saved: ' + r.error);
  return r.ok;
}

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

if (!AppMintKeystore.hasKey('diary')) {
  AppMintKeystore.generateKey('diary', { requireAuth: true, authValiditySeconds: 300 });
}

function saveDiary(entry) {
  const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(entry))));   // UTF-8 safe
  const r = WebToApkFS.writeSealed('diary.bin', b64, 'diary');
  if (r.ok) return true;
  if (r.error === 'auth-required') showMessage('Unlock your phone again, then tap Save.');
  else if (r.error === 'key-invalidated') showMessage('Your screen lock changed, so this key no longer works.');
  else showMessage('Could not save: ' + r.error);
  return false;
}

نکته‌ها: برای پاک کردن، «بازنویسی با صفر» نکنید: حافظهٔ فلش گوشی بلوک‌های قدیمی را نگه می‌دارد. قالب فایل فهرستی از بخش‌های AES-GCM با پیشوند طول است؛ فقط با WebToApkFS.readSealed و همان نام مستعار دوباره بخوانید. کلیدی که با {requireAuth:true} ساخته شده تا زمانی که کاربر اخیراً قفل را باز نکرده auth-required پاسخ می‌دهد.

downloads استاندارد وب#

<a download> and Blob downloads

نمونه

کد معمولی دانلود وب شما در اپلیکیشن مثل Chrome کار می‌کند. WebView خودش دانلودهای blob: و data: را نادیده می‌گیرد؛ اپلیکیشن آن‌ها را می‌گیرد و فایل را در پوشه‌ی Downloads گوشی با نام فایل خودتان ذخیره می‌کند، سپس «Downloaded <name> · Open» و اعلان «Download complete» را نشان می‌دهد.

مقدار بازگشتی: چیزی به صفحه برنمی‌گردد. پیش‌نیاز: چیزی برای روشن کردن نیست. دانلودهای آدرس وب (https://…) هم به پوشهٔ Downloads می‌روند (برای PDF یا تصویر ابتدا «مشاهده یا دانلود» پرسیده می‌شود)، و فایل‌های بسته‌بندی‌شده داخل اپلیکیشن (<a href="manual.pdf" download>) هم همین‌طور؛ در Android 9 و قدیمی‌تر، اپلیکیشن ابتدا مجوز حافظه را می‌خواهد. فایل‌های بزرگ مشکلی ندارند: فایل تکه‌تکه به اپلیکیشن استریم می‌شود.

<a download> با یک Blob - رایج‌ترین الگو:

<button id="save">Download report</button>
<script>
document.getElementById('save').addEventListener('click', function () {
  const blob = new Blob(['Name,Total\nAsha,120\n'], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'Report_2026-08.csv';     // this is the name the user sees
  document.body.appendChild(a);
  a.click();                             // caught by the app, saved to Downloads
  a.remove();
});
</script>

یک نشانی data:، FileSaver.js و navigator.msSaveBlob هم کار می‌کنند:

const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = 'Drawing.png';
a.click();

if (typeof saveAs === 'function') saveAs(blob, 'Backup.json');   // FileSaver.js
if (navigator.msSaveBlob) navigator.msSaveBlob(blob, 'Backup.json');

window.open یک URL از نوع blob: یا data: یعنی «نشانش بده»، نه «ذخیره‌اش کن»: PDF در نمایشگر PDF اپلیکیشن باز می‌شود و نوع‌های دیگر در برنامه‌ی گوشی برای آن نوع:

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

نکته‌ها: بدون نام، فایل با نام download.<ext> ذخیره می‌شود؛ فایل دوم با همان نام name (1).ext می‌شود. یک شیء File نام خودش را می‌آورد (URL.createObjectURL(file)). <a href="/files/x" download="Name.pdf"> در سایت خودتان Name.pdf را نگه می‌دارد (Chrome این ویژگی را روی پیوند سایت‌های دیگر نادیده می‌گیرد و اپلیکیشن هم همین‌طور). اگر هیچ اپلیکیشنی در گوشی نتواند یک فایل window.open را نشان دهد، به‌جای آن در Downloads ذخیره می‌شود. برای اینکه کاربر پوشه را انتخاب کند از showSaveFilePicker() استفاده کنید. برای ذخیره بدون عنصر <a> از AppMint.saveBlob(blob, name) یا AppMint.downloadFile(base64, name, mime).

input-file استاندارد وب#

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

نمونه

یک ورودی فایل معمولی در اپلیکیشن مثل Chrome کار می‌کند: انتخابگر عکس، دوربین یا انتخابگر فایل را باز می‌کند و صفحهٔ شما اشیای File معمولی می‌گیرد.

مقدار بازگشتی: رویداد change ورودی با input.files (وقتی کاربر لغو کند خالی است). پیش‌نیاز: عکس‌ها و فایل‌ها به چیزی نیاز ندارند. برای capture (دوربین) هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Camera را علامت بزنید؛ اپلیکیشن بار اول مجوز دوربین را می‌خواهد.

عکس‌ها - انتخابگر عکس Android را باز می‌کند (بدون درخواست مجوز؛ صفحه فقط آنچه را کاربر انتخاب کرده می‌گیرد):

<input type="file" id="pics" accept="image/*" multiple>
<div id="preview"></div>
<script>
document.getElementById('pics').addEventListener('change', function (e) {
  const box = document.getElementById('preview');
  for (let i = 0; i < e.target.files.length; i++) {
    const img = document.createElement('img');
    img.src = URL.createObjectURL(e.target.files[i]);
    img.style.width = '100px';
    box.appendChild(img);
  }
});
</script>

دوربین - capture انتخابگر را رد می‌کند و دوربین را مستقیم باز می‌کند:

<input type="file" id="snap" accept="image/*" capture="environment">
<input type="file" id="clip" accept="video/*" capture="environment">
<script>
document.getElementById('snap').addEventListener('change', function (e) {
  const photo = e.target.files[0];
  if (!photo) return;                        // user closed the camera
  const form = new FormData();
  form.append('photo', photo);
  fetch('/api/upload', { method: 'POST', body: form });
});
</script>

هر فایل، یا چند نوع (پسوندها هم کار می‌کنند):

<input type="file" accept=".pdf,.csv,application/json" multiple>
شما می‌نویسیدچه چیزی باز می‌شود
accept="image/*" / "video/*" / "image/*,video/*"انتخابگر عکس (عکس، ویدیو یا هر دو)
افزودن multipleکاربر می‌تواند چند مورد انتخاب کند
افزودن capture (با علامت‌خوردن Camera)مستقیم از دوربین - عکس برای image/*، ویدیو برای video/*
accept="image/*,.pdf" یا نه acceptانتخابگر فایل سیستم

نکته‌ها: بدون علامت‌خوردن Camera، capture نادیده گرفته می‌شود و انتخابگر باز می‌شود. وقتی Native Folder Access (SAF) علامت خورده باشد، اپلیکیشن به‌جای آن منوی خودش «انتخاب منبع بارگذاری» را نشان می‌دهد: گرفتن عکس / ضبط ویدیو (اگر Camera علامت خورده)، انتخاب فایل‌ها و بارگذاری پوشه (همهٔ فایل‌های یک پوشه را می‌فرستد). با علامت‌خوردن Storage در Android 12 و قدیمی‌تر، انتخاب فایلی غیر از عکس ممکن است ابتدا مجوز حافظه بخواهد. گوشی‌هایی که انتخابگر عکس ندارند انتخابگر فایل می‌گیرند که همان عکس‌ها را نشان می‌دهد.

showOpenFilePicker استاندارد وب#

showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access

نمونه

File System Access API استاندارد داخل اپلیکیشن، روی انتخابگر سند خودِ Android کار می‌کند: یک فایل را باز کنید و در همان فایل ذخیره کنید، با برگهٔ ذخیره‌به‌عنوان یک فایل بسازید، یا یک پوشهٔ کامل بگیرید تا در آن فایل بخوانید، بسازید و حذف کنید.

مقدار بازگشتی: showOpenFilePicker(options) ← یک Promise از آرایه‌ای از FileSystemFileHandle؛ showSaveFilePicker(options) ← یک FileSystemFileHandle؛ showDirectoryPicker(options) ← یک FileSystemDirectoryHandle. انتخابگرِ لغوشده AbortError را رد می‌کند؛ نبودنِ اقدام کاربر SecurityError را رد می‌کند؛ باز کردن انتخابگر دوم در حالی که یکی باز است NotAllowedError را رد می‌کند. پیش‌نیاز: چیزی برای روشن کردن و هیچ مجوزی لازم نیست؛ کاربر انتخاب می‌کند.

let handle = null;

document.getElementById('open').addEventListener('click', async () => {
  try {
    [handle] = await showOpenFilePicker({
      types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.md'] } }]
    });
    const file = await handle.getFile();
    editor.value = await file.text();
    title.textContent = file.name;
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});

document.getElementById('save').addEventListener('click', async () => {
  if (!handle) handle = await showSaveFilePicker({ suggestedName: 'note.txt' });
  const w = await handle.createWritable();   // writes go to a copy…
  await w.write(editor.value);
  await w.close();                           // …that replaces the file only here
});

document.getElementById('export').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  const out = await dir.getFileHandle('export.csv', { create: true });
  const w = await out.createWritable();
  await w.write(csvText);
  await w.close();
  for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);
});

چند عکس را یک‌جا انتخاب کنید، با شروع از پوشهٔ Pictures:

document.getElementById('add-photos').addEventListener('click', async () => {
  try {
    const handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ description: 'Images', accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
    for (const h of handles) {
      const img = document.createElement('img');
      img.src = URL.createObjectURL(await h.getFile());
      gallery.appendChild(img);
    }
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});

یک خط به انتهای یک فایل لاگ اضافه کنید، بدون بازنویسی آنچه از قبل هست:

async function appendLine(handle, line) {      // handle from showOpenFilePicker / showSaveFilePicker
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);
  await w.write(line + '\n');
  await w.close();
}

پوشهٔ پشتیبان را فقط یک‌بار انتخاب کنید و در هر اجرای بعدی به‌صورت خودکار در آن ذخیره کنید؛ دستگیره را در IndexedDB نگه دارید، دقیقاً مثل Chrome. اپلیکیشن مجوز پوشه را بین راه‌اندازی‌های مجدد حفظ می‌کند، پس queryPermission() پاسخ 'granted' می‌دهد و پشتیبان‌گیری نه به ضربه نیاز دارد و نه به انتخابگر:

// Tiny IndexedDB helpers (idb-keyval's get/set work the same way).
function db() {
  return new Promise((ok, fail) => {
    const r = indexedDB.open('backup-settings', 1);
    r.onupgradeneeded = () => r.result.createObjectStore('kv');
    r.onsuccess = () => ok(r.result);
    r.onerror = () => fail(r.error);
  });
}
async function kvGet(key) {
  const d = await db();
  return new Promise((ok, fail) => {
    const r = d.transaction('kv').objectStore('kv').get(key);
    r.onsuccess = () => ok(r.result);
    r.onerror = () => fail(r.error);
  });
}
async function kvSet(key, value) {
  const d = await db();
  return new Promise((ok, fail) => {
    const t = d.transaction('kv', 'readwrite');
    t.objectStore('kv').put(value, key);
    t.oncomplete = () => ok();
    t.onerror = () => fail(t.error);
  });
}

// "Choose backup folder" button (the picker needs a tap).
document.getElementById('choose-folder').addEventListener('click', async () => {
  try {
    const dir = await showDirectoryPicker({ mode: 'readwrite', startIn: 'documents' });
    await kvSet('backupDir', dir);
    status.textContent = 'Backups go to ' + dir.name;
  } catch (e) {
    if (e.name !== 'AbortError') status.textContent = 'Could not choose: ' + e.message;
  }
});

// Called after every workout, and on a timer: no tap needed once the folder is chosen.
async function autoBackup(data) {
  const dir = await kvGet('backupDir');
  if (!dir) return 'no folder chosen yet';
  if ((await dir.queryPermission({ mode: 'readwrite' })) !== 'granted') {
    return 'folder access was removed: tap "Choose backup folder" again';
  }
  const file = await dir.getFileHandle('backup-' + new Date().toISOString().slice(0, 10) + '.json', { create: true });
  const w = await file.createWritable();
  await w.write(JSON.stringify(data));
  await w.close();
  return 'saved ' + file.name;
}

اگر مجوز برداشته شده باشد (کاربر دسترسی اپلیکیشن را پاک کرده یا پوشه را حذف کرده)، queryPermission() پاسخ 'prompt' می‌دهد. با ضربه روی یک دکمه، requestPermission() انتخابگر پوشه را در همان پوشه دوباره باز می‌کند؛ انتخاب همان پوشه 'granted' برمی‌گرداند، هر چیز دیگر (یا لغو) 'denied':

document.getElementById('reconnect').addEventListener('click', async () => {
  const dir = await kvGet('backupDir');
  if (dir && (await dir.requestPermission({ mode: 'readwrite' })) === 'granted') {
    status.textContent = 'Backups go to ' + dir.name + ' again';
  } else {
    status.textContent = 'Choose a backup folder';
  }
});

پوشه‌ای را که کاربر انتخاب کرده پاک‌سازی کنید: فایل‌های قدیمیِ .tmp و زیرپوشه‌های با نام را حذف کنید cache:

document.getElementById('clean').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  let removed = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file' && entry.name.endsWith('.tmp')) {
      await dir.removeEntry(entry.name);
      removed++;
    } else if (entry.kind === 'directory' && entry.name === 'cache') {
      await dir.removeEntry(entry.name, { recursive: true });
      removed++;
    }
  }
  alert(removed + ' item(s) removed');
});

نکته‌ها: createWritable() از مدل فایل جایگزین (swap) مطابق مشخصات پیروی می‌کند - abort() (یا از بین رفتن صفحه) فایل اصلی را دست‌نخورده می‌گذارد؛ { keepExistingData: true } از محتوای فعلی شروع می‌کند؛ write({type:'seek'|'truncate'|'write', …})، seek() و truncate() کار می‌کنند. فایلی که اپلیکیشنش فقط‌خواندنی عرضه می‌کند با 'denied' به queryPermission({mode:'readwrite'}) پاسخ می‌دهد و createWritable() خطای NoModificationAllowedError را رد می‌کند. getFile() سند را استریم می‌کند، پس فایل‌های بزرگ مشکلی ندارند. دستگیره‌ها را می‌توان در IndexedDB ذخیره کرد (حتی داخل اشیا و آرایه‌ها) و پس از راه‌اندازی مجدد اپلیکیشن دوباره استفاده کرد: اپلیکیشن مجوز Android را برای هر پوشه و فایلی که انتخابگر برگردانده نگه می‌دارد (64 مورد اخیر؛ قدیمی‌ترین ابتدا آزاد می‌شود). queryPermission() تا وقتی آن مجوز نگه داشته شده 'granted' پاسخ می‌دهد و پس از از بین رفتنش 'prompt'؛ آن‌گاه requestPermission() به یک ضربه نیاز دارد و انتخابگر را در پوشهٔ خودِ دستگیره دوباره باز می‌کند. اپلیکیشن‌های ساخته‌شده پیش از 2026-10-03 دستگیره را با راه‌اندازی مجدد از دست می‌دادند؛ دوباره بسازید و کاربر یک بار دیگر پوشه را انتخاب می‌کند. types به فیلتر فایل انتخابگر تبدیل می‌شوند. فقط صفحهٔ سطح بالا انتخابگرها را دارد. انتقال‌های داخلی: __fsPick (با رویداد appmint:fs پاسخ داده می‌شود)، __fsFile، __fsList، __fsChild، __fsRemove، __fsPermission، __fsWritableOpen/Write/Truncate/Close/Abort. اپلیکیشن‌های ساخته‌شده با هوش مصنوعی بررسی می‌کنند can('fileHandles').

__dlAbort پل#

window.WebToApk.__dlAbort(token: String)

دانلود [token] را رها می‌کند (خواندن صفحه ناموفق بود یا منصرف شد).

نمونه

همراه با WebToApk.__dlBegin مستند شده - نمونهٔ آنجا این یکی را هم نشان می‌دهد.

انتقال داخلی پشت <a download>، دانلودهای Blob، window.open(blobUrl) و AppMint.saveBlob / AppMint.downloadFile: فایل تکه‌تکه از صفحه به اپلیکیشن استریم می‌شود، پس یک خروجی بزرگ هرگز یک رشتهٔ عظیم در حافظه نیست. صفحه‌ها از کد دانلود استاندارد استفاده می‌کنند؛ هرگز این‌ها را مستقیم صدا نزنید، چون متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: __dlBegin(name, type, size) یک توکن برمی‌گرداند (یا "" وقتی گوشی جا ندارد)؛ __dlChunk(token, base64) اگر نوشتن یک برش ممکن نبود false برمی‌گرداند؛ __dlEnd(token, action) تمام می‌کند (save = Downloads، open = نمایش آن، share، copy). پیش‌نیاز: هیچ.

از API عمومی استفاده کنید:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

یک نقاشی از canvas را از همان مسیر صادر کنید:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

یک پشتیبان JSON که کاربر می‌تواند نگه دارد ذخیره کنید، با پیامی وقتی تمام شد:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (ShellDownloads.kt).

__dlBegin پل#

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

یک دانلود به اندازهٔ [size] بایت با نام [name] ([type] = نوع MIME آن) باز می‌کند که صفحه آن را با __dlChunk استریم می‌کند. یک توکن برمی‌گرداند، یا "" وقتی جا نباشد.

نمونه

انتقال داخلی پشت <a download>، دانلودهای Blob، window.open(blobUrl) و AppMint.saveBlob / AppMint.downloadFile: فایل تکه‌تکه از صفحه به اپلیکیشن استریم می‌شود، پس یک خروجی بزرگ هرگز یک رشتهٔ عظیم در حافظه نیست. صفحه‌ها از کد دانلود استاندارد استفاده می‌کنند؛ هرگز این‌ها را مستقیم صدا نزنید، چون متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: __dlBegin(name, type, size) یک توکن برمی‌گرداند (یا "" وقتی گوشی جا ندارد)؛ __dlChunk(token, base64) اگر نوشتن یک برش ممکن نبود false برمی‌گرداند؛ __dlEnd(token, action) تمام می‌کند (save = Downloads، open = نمایش آن، share، copy). پیش‌نیاز: هیچ.

از API عمومی استفاده کنید:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

یک نقاشی از canvas را از همان مسیر صادر کنید:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

یک پشتیبان JSON که کاربر می‌تواند نگه دارد ذخیره کنید، با پیامی وقتی تمام شد:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (ShellDownloads.kt).

__dlChunk پل#

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

یک برش base64 از دانلود [token]؛ false وقتی انتقال ناموفق بود.

نمونه

همراه با WebToApk.__dlBegin مستند شده - نمونهٔ آنجا این یکی را هم نشان می‌دهد.

انتقال داخلی پشت <a download>، دانلودهای Blob، window.open(blobUrl) و AppMint.saveBlob / AppMint.downloadFile: فایل تکه‌تکه از صفحه به اپلیکیشن استریم می‌شود، پس یک خروجی بزرگ هرگز یک رشتهٔ عظیم در حافظه نیست. صفحه‌ها از کد دانلود استاندارد استفاده می‌کنند؛ هرگز این‌ها را مستقیم صدا نزنید، چون متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: __dlBegin(name, type, size) یک توکن برمی‌گرداند (یا "" وقتی گوشی جا ندارد)؛ __dlChunk(token, base64) اگر نوشتن یک برش ممکن نبود false برمی‌گرداند؛ __dlEnd(token, action) تمام می‌کند (save = Downloads، open = نمایش آن، share، copy). پیش‌نیاز: هیچ.

از API عمومی استفاده کنید:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

یک نقاشی از canvas را از همان مسیر صادر کنید:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

یک پشتیبان JSON که کاربر می‌تواند نگه دارد ذخیره کنید، با پیامی وقتی تمام شد:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (ShellDownloads.kt).

__dlEnd پل#

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

دانلود [token] را تمام می‌کند و روی آن عمل می‌کند: [action] «save» (در Downloads، با نوار «دانلود شد · باز کردن» و اعلان)، «open» (نمایشگر PDF یا اپلیکیشن گوشی برای آن نوع)، «share» (برگهٔ اشتراک‌گذاری) یا «copy» (تصویر در کلیپ‌بورد).

نمونه

همراه با WebToApk.__dlBegin مستند شده - نمونهٔ آنجا این یکی را هم نشان می‌دهد.

انتقال داخلی پشت <a download>، دانلودهای Blob، window.open(blobUrl) و AppMint.saveBlob / AppMint.downloadFile: فایل تکه‌تکه از صفحه به اپلیکیشن استریم می‌شود، پس یک خروجی بزرگ هرگز یک رشتهٔ عظیم در حافظه نیست. صفحه‌ها از کد دانلود استاندارد استفاده می‌کنند؛ هرگز این‌ها را مستقیم صدا نزنید، چون متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: __dlBegin(name, type, size) یک توکن برمی‌گرداند (یا "" وقتی گوشی جا ندارد)؛ __dlChunk(token, base64) اگر نوشتن یک برش ممکن نبود false برمی‌گرداند؛ __dlEnd(token, action) تمام می‌کند (save = Downloads، open = نمایش آن، share، copy). پیش‌نیاز: هیچ.

از API عمومی استفاده کنید:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

یک نقاشی از canvas را از همان مسیر صادر کنید:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

یک پشتیبان JSON که کاربر می‌تواند نگه دارد ذخیره کنید، با پیامی وقتی تمام شد:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (ShellDownloads.kt).

__fsChild پل#

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

getFileHandle / getDirectoryHandle (با `create`).

نمونه

انتقال داخلی پشت getFileHandle(name, {create}) و getDirectoryHandle(name, {create}). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز این را مستقیم صدا نزنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON به شکل {ok, kind, name, uri, canWrite} یا {ok:false, error} (NotFoundError، TypeMismatchError، NoModificationAllowedError). پیش‌نیاز: پوشه‌ای از showDirectoryPicker().

از API عمومی استفاده کنید:

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

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

async function exists(dir, name) {
  try {
    await dir.getFileHandle(name);                     // no {create} = look up only
    return true;
  } catch (e) {
    if (e.name === 'NotFoundError') return false;
    if (e.name === 'TypeMismatchError') return true;   // a folder already has that name
    throw e;
  }
}

تنظیمات اپلیکیشن را به‌صورت JSON در یک زیرپوشهٔ «config» ذخیره کنید و هر دو را در صورت نیاز بسازید:

async function saveSettings(root, settings) {
  const config = await root.getDirectoryHandle('config', { create: true });
  const file = await config.getFileHandle('settings.json', { create: true });
  const w = await file.createWritable();
  await w.write(JSON.stringify(settings, null, 2));
  await w.close();
}
// root = await showDirectoryPicker()  (from a button tap)

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsFile پل#

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

getFile(): `{ok, name, type, size, lastModified, url}` (بدنه از url جریان می‌یابد).

نمونه

انتقال داخلی پشت FileSystemFileHandle.getFile(). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز مستقیم آن را فراخوانی نکنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON به شکل {ok, name, type, size, lastModified, url} - بدنه از url استریم می‌شود (مبدأ خودِ اپلیکیشن، با محدوده‌ها) - یا {ok:false, error} (NotFoundError، TypeMismatchError، foreign_origin). پیش‌نیاز: دستگیره‌ای از یکی از انتخابگرها.

از API عمومی استفاده کنید:

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

یک تصویر انتخاب‌شده را روی صفحه نشان دهید بدون اینکه آن را در یک رشته بخوانید:

document.querySelector('#choose').addEventListener('click', async function () {
  const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': [] } }] });
  const file = await handle.getFile();                    // a real File, streamed from the app
  const img = document.querySelector('#preview');
  if (img.src) URL.revokeObjectURL(img.src);
  img.src = URL.createObjectURL(file);
  img.alt = file.name + ' (' + file.type + ')';
});

فایلی را که قبلاً باز کرده‌اید دوباره بخوانید و تغییر یا حذف آن را تشخیص دهید:

async function reloadIfChanged(handle, lastSeen) {
  try {
    const file = await handle.getFile();
    if (file.lastModified === lastSeen) return null;      // unchanged
    return { data: JSON.parse(await file.text()), lastModified: file.lastModified };
  } catch (e) {
    if (e.name === 'NotFoundError') alert(handle.name + ' was moved or deleted.');
    else throw e;
    return null;
  }
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsList پل#

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

ورودی‌های یک دستگیرهٔ پوشه: `{ok, entries:[{kind, name, uri, canWrite}]}`.

نمونه

انتقال داخلی پشت پیمایش یک FileSystemDirectoryHandle (entries()، keys()، values()، for await). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز این را مستقیم صدا نزنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON به شکل {ok, entries:[{kind, name, uri, canWrite}]} یا {ok:false, error}. پیش‌نیاز: پوشه‌ای از showDirectoryPicker().

از API عمومی استفاده کنید:

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

فایل‌های یک پوشه و همهٔ زیرپوشه‌هایش را بشمارید:

async function countFiles(dir) {
  let n = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file') n++;
    else n += await countFiles(entry);                 // entry is a FileSystemDirectoryHandle
  }
  return n;
}
document.querySelector('#scan').addEventListener('click', async function () {
  const dir = await showDirectoryPicker();
  console.log(dir.name, 'holds', await countFiles(dir), 'files');
});

فهرستی مرتب از عکس‌های یک پوشهٔ انتخاب‌شده توسط کاربر بسازید:

document.querySelector('#open-album').addEventListener('click', async function () {
  const dir = await showDirectoryPicker({ startIn: 'pictures' });
  const names = [];
  for await (const name of dir.keys()) {
    if (/\.(jpe?g|png|webp)$/i.test(name)) names.push(name);
  }
  names.sort(function (a, b) { return a.localeCompare(b); });
  renderAlbum(dir, names);                             // dir.getFileHandle(name) opens each one
});

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsPermission پل#

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

queryPermission() / requestPermission(): `{ok, state:'granted'|'prompt'}` برای دستگیره‌ای که ریشهٔ انتخاب‌شده‌اش [rootUri] است - تا وقتی اپلیکیشن مجوز آن انتخاب را دارد، حتی پس از راه‌اندازی‌های مجدد، 'granted' (مجوز ذخیره می‌شود؛ دستگیره از IndexedDB برمی‌گردد).

نمونه

انتقال داخلی پشت FileSystemHandle.queryPermission() و requestPermission(). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز این را مستقیم صدا نزنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON به شکل {ok:true, state:'granted'} تا وقتی اپلیکیشن مجوز پوشه یا فایل انتخاب‌شده‌ای را که دستگیره از آن آمده نگه دارد (بین راه‌اندازی‌های مجدد حفظ می‌شود)، {ok:true, state:'prompt'} وقتی مجوز از بین رفته باشد، یا {ok:false, error:'foreign_origin'}. فایل فقط‌خواندنی در صفحه، بدون هیچ فراخوانی، برای {mode:'readwrite'} پاسخ 'denied' می‌دهد. پیش‌نیاز: دستگیره‌ای از یکی از انتخابگرها، یا دستگیره‌ای که از IndexedDB دوباره خوانده شده.

از API عمومی استفاده کنید:

const dir = await kvGet('backupDir');                 // a handle stored in IndexedDB earlier
if ((await dir.queryPermission({ mode: 'readwrite' })) === 'granted') {
  const f = await dir.getFileHandle('backup.json', { create: true });
  const w = await f.createWritable();
  await w.write(JSON.stringify(state));
  await w.close();
}

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

document.getElementById('reconnect').addEventListener('click', async () => {
  const dir = await kvGet('backupDir');
  const state = await dir.requestPermission({ mode: 'readwrite' });   // 'granted' | 'denied'
  status.textContent = state === 'granted' ? 'Backups resumed' : 'Choose a backup folder';
});

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt). برای کل File System Access API ببینید showOpenFilePicker.

__fsPick پل#

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

انتخابگر [kind] (open | save | directory) را باز می‌کند؛ دستگیره‌ها به‌صورت `appmint:fs`.

نمونه

انتقال داخلی پشت showOpenFilePicker()، showSaveFilePicker() و showDirectoryPicker(). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز این را مستقیم صدا نزنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: {ok:true} وقتی انتخابگر روی صفحه است، یا {ok:false, error} (NotAllowedError وقتی یکی از قبل باز است، foreign_origin). دستگیره‌ها به‌صورت رویداد appmint:fs می‌رسند {requestId, handles:[{kind, name, uri, canWrite}]} یا {requestId, error:'cancelled'}. پیش‌نیاز: چیزی برای روشن کردن نیست.

از API عمومی استفاده کنید:

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

یک گزارش را با نام پیشنهادی از طریق ضربه روی دکمه ذخیره کنید (انتخابگرها به اقدام کاربر نیاز دارند):

document.querySelector('#export').addEventListener('click', async function () {
  try {
    const handle = await showSaveFilePicker({
      suggestedName: 'report.csv',
      types: [{ description: 'CSV', accept: { 'text/csv': ['.csv'] } }]
    });
    const w = await handle.createWritable();
    await w.write(buildCsv());
    await w.close();
  } catch (e) {
    if (e.name !== 'AbortError') alert('Export failed: ' + e.message);   // AbortError = cancelled
  }
});

به کاربر اجازه دهید چند عکس را یک‌جا انتخاب کند، با شروع از پوشهٔ Pictures:

document.querySelector('#add-photos').addEventListener('click', async function () {
  let handles;
  try {
    handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
  } catch (e) { return; }                                 // cancelled or already open
  for (const h of handles) addToGallery(await h.getFile());
});

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt). همان کد صفحه بدون تغییر در Chrome دسکتاپ هم کار می‌کند.

__fsRemove پل#

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

removeEntry(name, {recursive}).

نمونه

انتقال داخلی پشت FileSystemDirectoryHandle.removeEntry(name, {recursive}). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز مستقیم آن را فراخوانی نکنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON به شکل {ok:true} یا {ok:false, error} (NotFoundError، InvalidModificationError برای پوشهٔ غیرخالی بدون recursive). پیش‌نیاز: پوشه‌ای از showDirectoryPicker().

از API عمومی استفاده کنید:

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

یک فایل را حذف کنید و «قبلاً حذف شده» را به‌منزلهٔ انجام‌شده بگیرید:

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

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

async function removeFolder(dir, name) {
  try {
    await dir.removeEntry(name);                          // succeeds for an empty folder
  } catch (e) {
    if (e.name !== 'InvalidModificationError') throw e;
    if (!confirm('"' + name + '" is not empty. Delete it and everything inside?')) return false;
    await dir.removeEntry(name, { recursive: true });
  }
  return true;
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsWritableAbort پل#

window.WebToApk.__fsWritableAbort(token: String)

abort(): جایگزینی کنار گذاشته می‌شود؛ سند دست‌نخورده می‌ماند.

نمونه

انتقال داخلی پشت FileSystemWritableFileStream.abort() - نسخهٔ جایگزین دور ریخته می‌شود و فایل همان‌طور که بود می‌ماند. صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز این را مستقیم صدا نزنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: هیچ. پیش‌نیاز: یک جریان نوشتنیِ باز.

از API عمومی استفاده کنید:

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

به کاربر اجازه دهید یک خروجی طولانی را لغو کند؛ هیچ چیز نیمه‌نوشته به فایل نمی‌رسد:

let cancelled = false;
document.getElementById('cancel').onclick = function () { cancelled = true; };

async function exportAll(handle, pages) {
  const w = await handle.createWritable();
  for (const page of pages) {
    if (cancelled) { await w.abort(); return 'cancelled'; }   // the file keeps its old content
    await w.write(await renderPage(page));
  }
  await w.close();
  return 'saved';
}

با pipeTo مستقیم در یک فایل دانلود کنید؛ متوقف کردن دانلود نوشتن را لغو می‌کند:

const ctl = new AbortController();
document.getElementById('stop').onclick = function () { ctl.abort(); };

async function downloadInto(handle, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error('HTTP ' + res.status);
  const w = await handle.createWritable();
  try {
    await res.body.pipeTo(w, { signal: ctl.signal });     // closes the file when the body ends
    return 'saved';
  } catch (e) {
    return e.name === 'AbortError' ? 'stopped, file unchanged' : 'failed: ' + e.message;
  }
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsWritableClose پل#

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

close(): نسخهٔ جایگزین جای سند را می‌گیرد.

نمونه

انتقال داخلی پشت FileSystemWritableFileStream.close() - نسخهٔ جایگزین فایل را عوض می‌کند. صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز این را مستقیم صدا نزنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON {ok:true} یا {ok:false, error} (NoModificationAllowedError، InvalidModificationError وقتی برنامه‌ی آن فایل نمی‌تواند آن را کوتاه کند). پیش‌نیاز: یک جریان نوشتنی باز.

از API عمومی استفاده کنید:

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

به کاربر بگویید چرا ذخیره انجام نشد (close() جایی است که ممکن است شکست بخورد):

async function saveText(handle, text) {
  const w = await handle.createWritable();
  await w.write(text);
  try {
    await w.close();
    return true;
  } catch (e) {
    if (e.name === 'InvalidModificationError') alert('This file\'s app cannot make it shorter. Save a copy instead.');
    else if (e.name === 'NoModificationAllowedError') alert('This file cannot be changed.');
    else alert('Save failed: ' + e.message);
    return false;
  }
}

یک فایل را در چند بخش بنویسید؛ خواننده‌ها تا زمانی که آن یکی همچنان محتوای قدیمی را می‌بینند close():

async function saveRows(handle, rows) {
  const w = await handle.createWritable();
  await w.write('id,name,total\n');
  for (const r of rows) await w.write(r.id + ',' + r.name + ',' + r.total + '\n');
  console.log('before close:', (await handle.getFile()).size, 'bytes (old file)');
  await w.close();
  console.log('after close:', (await handle.getFile()).size, 'bytes');
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsWritableOpen پل#

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

createWritable(): یک فایل swap، که وقتی [keepExisting] باشد از فایل کپی می‌شود.

نمونه

انتقال داخلی پشت FileSystemFileHandle.createWritable({keepExistingData}). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز مستقیم آن را فراخوانی نکنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON به شکل {ok, token} برای یک نسخهٔ جایگزین خصوصی، یا {ok:false, error} (NoModificationAllowedError برای فایل فقط‌خواندنی). پیش‌نیاز: دستگیره‌ای از یکی از انتخابگرها.

از API عمومی استفاده کنید:

const handle = await showSaveFilePicker({ suggestedName: 'report.csv' });
const w = await handle.createWritable({ keepExistingData: false });
await w.write('a,b\n1,2\n');
await w.close();

یک خط به انتهای یک فایل لاگ موجود اضافه کنید و محتوای فعلی آن را نگه دارید:

async function appendLine(handle, line) {
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                     // write after the old content
  await w.write(new Date().toISOString() + ' ' + line + '\n');
  await w.close();                                        // only now does the file change
}

یک canvas را به‌صورت PNG ذخیره کنید و اگر چیزی خراب شد فایل را دست‌نخورده بگذارید:

document.querySelector('#save-png').addEventListener('click', async function () {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (r) { document.querySelector('canvas').toBlob(r, 'image/png'); });
  const w = await handle.createWritable();
  try {
    await w.write(blob);
    await w.close();
  } catch (e) {
    try { await w.abort(); } catch (x) {}                 // nothing reaches the file before close()
    alert('Could not save: ' + e.message);
  }
});

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsWritableTruncate پل#

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

truncate(size) روی یک جریان نوشتنی.

نمونه

انتقال داخلی پشت FileSystemWritableFileStream.truncate(size). صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز مستقیم آن را فراخوانی نکنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON {ok:true} یا {ok:false, error}. پیش‌نیاز: یک جریان نوشتنی باز.

از API عمومی استفاده کنید:

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

فقط 1,000 بایت اول یک فایل را نگه دارید و هرچه بعد از آن است ببُرید:

async function keepFirstKilobyte(handle) {
  const w = await handle.createWritable({ keepExistingData: true });
  await w.truncate(1000);   // a file shorter than this grows instead (see the next example)
  await w.close();
}

فضا رزرو کنید: کوتاه‌کردن به اندازه‌ای بزرگ‌تر، فایل را بزرگ می‌کند و بایت‌های جدید هنگام خواندن صفر هستند:

async function makeEmptySlotFile(handle, slots, slotSize) {
  const w = await handle.createWritable();
  await w.truncate(slots * slotSize);                  // e.g. 100 slots of 256 bytes, all zero
  await w.write({ type: 'write', position: 0, data: new Uint8Array([1, 0, 0, 0]) });
  await w.close();
  console.log('file is now', (await handle.getFile()).size, 'bytes');
}

نکته‌ها: در پوستهٔ اپلیکیشن پیاده‌سازی شده (FileSystemAccess.kt).

__fsWritableWrite پل#

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

یک تکه از write() یک جریان نوشتنی، در [position].

نمونه

انتقال داخلی پشت FileSystemWritableFileStream.write() - یک تکه در یک موقعیت در نسخهٔ جایگزین. صفحه‌ها از API استاندارد استفاده می‌کنند؛ هرگز این را مستقیم صدا نزنید - متدهای __ ممکن است بدون اطلاع تغییر کنند.

مقدار بازگشتی: JSON {ok:true} یا {ok:false, error}. پیش‌نیاز: یک جریان نوشتنی باز.

از API عمومی استفاده کنید:

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

یک خط به انتهای فایل لاگی که کاربر انتخاب کرده اضافه کنید (انتخابگر فایل به یک ضربه نیاز دارد):

document.getElementById('addLine').addEventListener('click', async () => {
  const [handle] = await showOpenFilePicker({
    types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.log'] } }]
  });
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                   // start writing after the old content
  await w.write(new Date().toISOString() + ' checked in\n');
  await w.close();
});

یک نقاشی canvas را به‌صورت PNG جدید با «ذخیره به‌عنوان» ذخیره کنید (یک Blob به‌صورت بایت‌های خامش نوشته می‌شود):

document.getElementById('saveDrawing').addEventListener('click', async () => {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ description: 'PNG image', accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (ok) { document.querySelector('canvas').toBlob(ok, 'image/png'); });
  const w = await handle.createWritable();
  await w.write(blob);
  await w.close();
});

نکته‌ها: در پوسته‌ی اپلیکیشن پیاده‌سازی شده است (FileSystemAccess.kt). داده‌های بزرگ به‌صورت تکه‌تکه ارسال می‌شوند.

getMaxIoChunkSize پل#

window.WebToApk.getMaxIoChunkSize(): Long

سقف بایت در هر فراخوانی برای [readFileBase64] / [writeFileBase64]، برای محاسبهٔ تکه‌ها.

نمونه

حد بایت در هر فراخوانی برای readFileBase64 / writeFileBase64 (4 مگابایت). صفحه‌ها معمولاً از WebToApkFS.maxChunkSize().

مقدار بازگشتی: (همگام) یک عدد (4194304). پیش‌نیاز: برای صدا زدنش هیچ؛ متدهای خواندن/نوشتنی که توصیف می‌کند به علامت‌خوردن Native Folder Access (SAF) در مرحله ۴ (دسترسی) نیاز دارند.

روش عمومی:

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

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

let step = 1048576;
if (window.WebToApk && typeof WebToApk.getMaxIoChunkSize === 'function') {
  step = Number(WebToApk.getMaxIoChunkSize()) || step;
}
console.log('read/write up to', step, 'bytes per call');

محاسبه کنید که یک فایل چند فراخوانی پل لازم دارد، تا پیش از کپی نوار پیشرفت را متناسب کنید:

function plannedSteps(fileUri) {
  const step = WebToApkFS.maxChunkSize();          // 0 when the helper is not available
  const info = WebToApkFS.stat(fileUri);
  if (!step || !info.ok) return 0;
  const steps = Math.max(1, Math.ceil(info.size / step));
  document.querySelector('#progress').max = steps;
  return steps;
}

یک فایل را در فایل دیگر کپی کنید، هر بار یک تکه با حداکثر اندازه:

function copyFile(fromUri, toPath) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
    if (!w.ok) return w;
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, bytes: offset };
  }
}
// copyFile(pickedUri, 'photo-backup.jpg')  (a file in the granted folder)

نکته‌ها: نوشتنِ بزرگ‌تر از این با chunk too large (max N bytes) رد می‌شود؛ خواندنِ بزرگ‌تر به این اندازه بریده می‌شود.

pickFile پل#

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

انتخابگر تک‌فایلِ سیستم را باز می‌کند (ACTION_OPEN_DOCUMENT). برخلاف [requestFolderAccess] این فقط به یک فایل دسترسی می‌دهد، که چیزی است که یک ویرایشگر یا خردکن باید بخواهد؛ مجوز خواندن/نوشتن ذخیره می‌شود تا نشانی پس از بسته شدن پردازه همچنان کار کند. پاسخ روی `WebToApkOnFilePickResult(success, fileJson, requestId)` می‌رسد؛ `error` در شکست یکی از «cancelled»، «disabled»، «no-picker»، «foreign_origin» یا «busy» است (انتخابگری از قبل برای درخواست قبلی باز است که پاسخ خودش را نگه می‌دارد). @param mimeFilter مثلاً "image/\*"، "application/pdf"، یا "" برای هر فایل. (ستاره فقط به این دلیل با بک‌اسلش نوشته شده که توضیحات بلوکی Kotlin تودرتو می‌شوند.)

نمونه

انتخابگر سیستم را برای ONE فایل باز می‌کند. صفحه‌ها معمولاً از کمک‌تابع Promise یعنی WebToApkFS.pickFile(mimeFilter) استفاده می‌کنند؛ این متد خام از طریق callback پاسخ می‌دهد.

مقدار بازگشتی: فعلاً چیزی نیست. بعداً پوسته window.WebToApkOnFilePickResult(success, fileJson, requestId) را صدا می‌زند، که در آن fileJson یک رشتهٔ JSON به شکل {ok, uri, name, size, mime} در صورت موفقیت است، یا {ok:false, uri:'', error} با error = 'cancelled'، 'disabled'، 'no-picker'، 'busy' یا 'foreign_origin'. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

روش عمومی (توصیه‌شده):

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

فراخوانی خام، با تطبیق توسط خودتان requestId:

function pickRaw(mime) {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof WebToApk.pickFile !== 'function') { resolve(null); return; }
    const myId = 'pick_' + Date.now();
    window.WebToApkOnFilePickResult = function (success, fileJson, requestId) {
      if (requestId !== myId) return;
      const r = JSON.parse(fileJson);           // fileJson is a string
      resolve(success && r.ok ? r : null);      // null = cancelled or not allowed
    };
    WebToApk.pickFile(myId, mime || '');        // '' = any file
  });
}

یک PDF ضمیمه کنید و نام و اندازه‌اش را نشان دهید، و لغو را از مشکل واقعی تشخیص دهید:

document.querySelector('#attach').addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('application/pdf');
  if (!f.ok) {
    if (f.error === 'cancelled' || f.error === 'busy') return;   // user closed it / one already open
    alert('Could not open the file picker: ' + f.error);         // 'disabled', 'no-picker', ...
    return;
  }
  document.querySelector('#attachment').textContent =
    f.name + ' (' + Math.round(f.size / 1024) + ' KB)';
  localStorage.setItem('attachmentUri', f.uri);   // the grant is kept across restarts
});

یک فایل متنی را باز کنید و محتوایش را در یک ویرایشگر بارگذاری کنید:

async function openTextFile() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);            // {ok, base64, size} or {ok:false, error}
  if (!r.ok) { alert('Could not read ' + f.name + ': ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  document.querySelector('#editor').value = new TextDecoder('utf-8').decode(bytes);
  document.querySelector('#editor').dataset.uri = f.uri;   // save back to the same file later
}

نکته‌ها: هم‌زمان فقط یک انتخاب می‌تواند باز باشد: فراخوانی‌ای که هنگام باز بودن یکی انجام شود فوراً با error:'busy' پاسخ می‌گیرد و انتخاب باز پاسخ خودش را نگه می‌دارد. مجوز (خواندن و نوشتن) تا وقتی releaseFileAccess(uri) را فراخوانی نکنید نگه داشته می‌شود. فایل اینجا خوانده نمی‌شود؛ از readFileBase64 / WebToApkFS.readBytes با uri.

readFileBase64 پل#

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

حداکثر [length] بایت خام را از [offset] می‌خواند و به‌صورت base64 برمی‌گرداند. @param pathOrUri یک URI `content://` از [pickFile]، یا مسیری نسبت به پوشه‌ی مجاز از [requestFolderAccess]. @param length تعداد بایت برای خواندن؛ در هر فراخوانی به 4 MB محدود می‌شود. 0 یا منفی یعنی «تا جایی که یک فراخوانی اجازه می‌دهد». @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` به حلقه‌ی تکه‌تکه‌خوانی می‌گوید کی بایستد، بدون اینکه خودش با `size` مقایسه کند.

نمونه

بایت‌های خام را از یک فایل، از یک آفست بایتی می‌خواند و به‌صورت base64 برمی‌گرداند. صفحه‌ها معمولاً از WebToApkFS.readBytes(pathOrUri, offset, length) استفاده می‌کنند (همان نتیجه، از قبل تجزیه‌شده) یا WebToApkFS.readAll(pathOrUri).

مقدار بازگشتی: (همگام) یک رشتهٔ JSON به شکل {ok:true, base64, bytesRead, offset, size, eof} یا {ok:false, error}. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

روش عمومی:

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

فراخوانی خام - توجه کنید که نتیجه رشته‌ای است که باید JSON.parse کنید، نه base64 خالی:

if (window.WebToApk && typeof WebToApk.readFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.readFileBase64(fileUri, 0, 0));   // length 0 = up to 4 MB
  if (r.ok) {
    const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
    console.log('first byte', bytes[0], 'of', r.size);
  } else {
    console.log('read failed:', r.error);
  }
}

بررسی کنید فایل انتخاب‌شده واقعاً PNG است، با خواندن فقط 8 بایت اولش:

function isPng(fileUri) {
  const r = WebToApkFS.readBytes(fileUri, 0, 8);
  if (!r.ok || r.bytesRead < 8) return false;
  const head = atob(r.base64);
  const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
  return sig.every(function (b, i) { return head.charCodeAt(i) === b; });
}

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

function scanInChunks(fileUri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fileUri, offset, step);
    if (!r.ok) throw new Error('read failed: ' + r.error);
    onChunk(r.base64);                                    // process this piece, then drop it
    offset += r.bytesRead;
    document.querySelector('#progress').value = r.size ? offset / r.size : 1;
    if (r.eof || r.bytesRead <= 0) break;
  }
  return offset;                                          // total bytes read
}

نکته‌ها: pathOrUri یک نشانی content:// از pickFile یا مسیری نسبت به پوشه‌ای از requestFolderAccess است. حداکثر 4 مگابایت در هر فراخوانی (getMaxIoChunkSize())؛ روی bytesRead حلقه بزنید تا eof. خطاها: not-found، disabled، cannot-open، offset must be >= 0، offset past end of file (N bytes)، foreign_origin (صفحه‌ای از سایتی غیر از سایت خودِ اپلیکیشن).

releaseFileAccess پل#

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

یک مجوز تک‌فایلِ ذخیره‌شده را که [pickFile] گرفته آزاد می‌کند. Android تعداد مجوزهای URI را که یک اپلیکیشن می‌تواند ذخیره کند محدود می‌کند، پس اپلیکیشنی که فایل‌های زیادی انتخاب می‌کند باید مواردی را که تمام کرده آزاد کند.

نمونه

مجوز خواندن/نوشتنِ نگه‌داشته‌شده برای فایلی را که با pickFile انتخاب شده آزاد می‌کند. صفحه‌ها معمولاً از WebToApkFS.releaseFile(uri) استفاده می‌کنند که یک مقدار منطقی واقعی برمی‌گرداند.

مقدار بازگشتی: (همگام) رشتهٔ "true" یا "false". پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید (در غیر این صورت همیشه "false").

روش عمومی:

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

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

if (window.WebToApk && typeof WebToApk.releaseFileAccess === 'function') {
  const released = WebToApk.releaseFileAccess(fileUri) === 'true';   // a string, not a boolean
  console.log(released ? 'access released' : 'nothing to release');
}

فهرست «فایل‌های اخیر» با حداکثر 10 مورد نگه دارید و مجوز هر فایلی را که از آن بیرون می‌افتد آزاد کنید:

function rememberRecent(uri) {
  let recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  recent = [uri].concat(recent.filter(function (u) { return u !== uri; }));
  while (recent.length > 10) {
    const dropped = recent.pop();
    if (window.WebToApkFS) WebToApkFS.releaseFile(dropped);   // Android caps kept grants
  }
  localStorage.setItem('recentFiles', JSON.stringify(recent));
}

سند فعلی را ببندید: مجوزش را آزاد کنید و نشانی ذخیره‌شده را فراموش کنید:

document.querySelector('#close-doc').addEventListener('click', function () {
  const uri = localStorage.getItem('openDocUri');
  if (uri && window.WebToApkFS) {
    const released = WebToApkFS.releaseFile(uri);   // true / false, already a boolean
    console.log(released ? 'grant released' : 'no grant was held for this file');
  }
  localStorage.removeItem('openDocUri');
  document.querySelector('#editor').value = '';
});

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

statFile پل#

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

فراداده‌های فایل یا پوشه بدون خواندن چیزی از آن. مسیر نسبی نام یک فایل یا زیرپوشهٔ پوشهٔ مجاز است؛ یک پوشه با `isDirectory: true`، `size` برابر 0 و `mime` برابر `vnd.android.document/directory` پاسخ می‌دهد. @return JSON به شکل `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`، یا `{ok:false, error}` با `not-found`، `disabled` یا `foreign_origin`.

نمونه

فراداده‌های یک فایل (نام، اندازه، نوع) را بدون خواندن بایت‌هایش می‌خواند. صفحه‌ها معمولاً از WebToApkFS.stat(pathOrUri) استفاده می‌کنند که JSON را برای شما تجزیه می‌کند.

مقدار بازگشتی: (همگام) یک رشتهٔ JSON به شکل {ok, name, size, mime, lastModified, uri, isDirectory, canWrite}، یا {ok:false, error} ('not-found'، 'disabled'، 'foreign_origin'). پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

روش عمومی:

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

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

if (window.WebToApk && typeof WebToApk.statFile === 'function') {
  const info = JSON.parse(WebToApk.statFile(fileUri));   // or 'notes/today.txt' in the granted folder
  if (info.ok) console.log(info.name, info.size + ' bytes', info.mime, info.canWrite);
  else console.log('stat failed:', info.error);
}

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

const MAX_BYTES = 20 * 1024 * 1024;
function canImport(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) { alert('File not available: ' + info.error); return false; }
  if (info.size > MAX_BYTES) {
    alert(info.name + ' is ' + Math.round(info.size / 1048576) + ' MB; the limit is 20 MB.');
    return false;
  }
  return true;
}

یک زیرپوشهٔ پوشهٔ مجاز را پیدا کنید و فقط وقتی قابل‌نوشتن است «ذخیره در اینجا» را نشان دهید:

function checkExportFolder() {
  const info = WebToApkFS.stat('exports');          // a path relative to the granted folder
  const btn = document.querySelector('#save-here');
  if (!info.ok) {                                    // 'not-found' = the folder is not there yet
    btn.textContent = 'Create "exports" and save';
    return WebToApkFS.mkdir('exports');
  }
  btn.disabled = !(info.isDirectory && info.canWrite);
  console.log('exports last changed', new Date(info.lastModified).toLocaleString());
  return true;
}

نکته‌ها: یک نشانی content:// از pickFile یا مسیری نسبت به پوشه‌ای را که با requestFolderAccess مجاز شده می‌پذیرد. فقط به صفحه‌های خودِ اپلیکیشن یا وب‌سایت پیکربندی‌شدهٔ آن پاسخ می‌دهد؛ صفحه‌ای از هر سایت دیگر می‌گیرد foreign_origin.

writeFileBase64 پل#

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

بایت‌های خام را می‌نویسد و تأیید می‌کند چند بایت نوشته شد. @param mode `"patch"` در [offset] می‌نویسد و بقیه‌ی فایل را دست‌نخورده می‌گذارد (آنچه بازنویسی چندمرحله‌ای لازم دارد)، `"truncate"` فایل را جایگزین می‌کند، `"append"` [offset] را نادیده می‌گیرد و در انتها می‌نویسد. @return JSON `{ok, bytesWritten, offset, size, error}` - `size` بعد از fsync دوباره از فایل خوانده می‌شود، تا فراخواننده بتواند نوشتن را واقعاً تأیید کند، نه اینکه به یک boolean اعتماد کند.

نمونه

بایت‌های base64 را در یک فایل در یک آفست (patch)، روی کل فایل (truncate) یا در انتها (append) می‌نویسد. صفحه‌ها معمولاً از WebToApkFS.writeBytes(pathOrUri, base64, offset, mode) یا WebToApkFS.writeAll(pathOrUri, base64).

مقدار بازگشتی: (همگام) یک رشتهٔ JSON به شکل {ok, bytesWritten, offset, size} (size پس از fsync دوباره خوانده می‌شود) یا {ok:false, error}. پیش‌نیاز: هنگام ساخت، در مرحله ۴ (دسترسی) گزینهٔ Native Folder Access (SAF) را علامت بزنید.

روش عمومی:

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

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

if (window.WebToApk && typeof WebToApk.writeFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.writeFileBase64('notes/today.txt', btoa('Buy milk\n'), 0, 'append'));
  if (!r.ok) console.log('write failed:', r.error);
  else console.log('file is now', r.size, 'bytes');
}

یک نقاشی canvas را به‌صورت PNG در فایلی که کاربر انتخاب کرده ذخیره کنید و آنچه را آنجا بود جایگزین کنید:

document.querySelector('#save').addEventListener('click', async function () {
  const f = await WebToApkFS.pickFile('image/png');
  if (!f.ok) return;
  const base64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
  const r = WebToApkFS.writeAll(f.uri, base64);   // truncates, then writes in 4 MB chunks
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes to', f.name);
  else alert('Save failed: ' + r.error);
});

یک فایل را قبل از حذف، درجا با صفر بازنویسی کنید و تأیید کنید که هر مرحله انجام شده:

function zeroFill(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) return false;
  const slice = 32768;
  for (let at = 0; at < info.size; at += slice) {
    const len = Math.min(slice, info.size - at);
    const zeros = btoa(String.fromCharCode.apply(null, new Uint8Array(len)));
    const w = WebToApkFS.writeBytes(fileUri, zeros, at, 'patch');   // the rest of the file is kept
    if (!w.ok || w.bytesWritten !== len) { console.log('write failed at', at, w.error); return false; }
  }
  return true;   // each call is fsync'ed before it answers
}

نکته‌ها: حالت 'patch' (پیش‌فرض وقتی خالی است)، 'truncate' یا 'append' است. هر فراخوانی حداکثر 4 MB بایت رمزگشایی‌شده می‌گیرد (getMaxIoChunkSize()). مسیر نسبی در صورت نبودن داخل پوشه‌ی مجاز ساخته می‌شود. خطاها: disabled، foreign_origin (صفحه‌ی روی صفحه از خود اپلیکیشن نیست)، bad mode: x, offset must be >= 0, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.