أنشئ APK مجانًا

واجهة JavaScript bridge API

الملفات: ملف واحد في كل مرة

أداة الاختيار في النظام وقراءة/كتابة ثنائية على أجزاء، للملفات الأكبر من أن تُحفظ في سلسلة.

AppMint.downloadFile دالة مساعدة (helper)#

AppMint.downloadFile(data, fileName, mimeType?)

مثال

يحفظ ملفًا أنشأته صفحتك - من سلسلة base64 أو عنوان data: أو عنوان blob: أو Blob - بالاسم الذي تختاره. تُفتح ورقة "حفظ باسم…" في Android وقد مُلئ فيها ذلك الاسم.

يُرجع: (متزامن) true بمجرد تسليم الحفظ إلى Android (false فقط لعنوان data: تعذّر تحليله). يرمي TypeError إذا لم يكن data Blob أو سلسلة. ما يزال المستخدم يختار المجلد ويمكنه الإلغاء. المتطلبات: لا شيء - موجود دائمًا في التطبيق.

سلسلة 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 عادية. إذا لم يكن في الهاتف شاشة "حفظ باسم…" أصلًا، يُكتب الملف مباشرة في التنزيلات. كما يعمل كودك المعتاد <a download> - راجع downloads.

AppMint.saveBlob دالة مساعدة (helper)#

AppMint.saveBlob(blob, fileName)

مثال

يحفظ Blob (أو File) بالاسم الذي تختاره. تُفتح ورقة "حفظ باسم…" في Android وقد مُلئ فيها ذلك الاسم.

يُرجع: (متزامن) true بمجرد تسليم الحفظ إلى Android. يرمي TypeError عندما لا يكون الوسيط الأول Blob. المتطلبات: لا شيء - موجود دائمًا في التطبيق.

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 دفعة واحدة، لذا قد تستنفد الكتل الكبيرة جدًا (مئات الميغابايت) الذاكرة. يختار المستخدم المجلد وقد يلغي؛ ولا شيء يخبر الصفحة بما حدث.

AppMint.setDownloadName دالة مساعدة (helper)#

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 دالة مساعدة (helper)#

WebToApkFS.maxChunkSize()

مثال

يخبرك بأقصى عدد من البايتات يمكن لاستدعاء readBytes أو writeBytes واحد نقله (4 ميغابايت)، لتحدد حجم حلقة الأجزاء.

القيمة المُرجَعة: (متزامن) عدد من البايتات، أو 0 عندما تكون دالة الجسر مفقودة. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء (فهذا ما يحقن 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 دالة مساعدة (helper)#

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) في الخطوة 4 (الوصول) عند الإنشاء.

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 دالة مساعدة (helper)#

WebToApkFS.readAll(pathOrUri)

مثال

يقرأ ملفًا كاملًا في سلسلة base64 واحدة. يكرّر استدعاء readBytes نيابةً عنك، 4 ميغابايت في كل مرة.

يُرجع: (متزامن) {ok:true, base64, size}، أو {ok:false, error} الخاص بأول جزء فشل. المتطلبات: فعّل Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.

حمّل صورة مختارة في <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 دالة مساعدة (helper)#

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) في الخطوة 4 (الوصول) عند الإنشاء.

اقرأ أول 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. الاستدعاء متزامن، فأبقِ الأجزاء معتدلة على خيط الواجهة. للملفات الصغيرة يتولى WebToApkFS.readAll(uri) الحلقة نيابةً عنك.

WebToApkFS.readSealed دالة مساعدة (helper)#

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' …). المتطلبات: حدّد Native Folder Access (SAF) والمفاتيح الآمنة (التشفير) في الخطوة 4 (الوصول) عند الإنشاء.

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 دالة مساعدة (helper)#

WebToApkFS.releaseFile(uri)

مثال

يُعيد الوصول طويل الأمد الذي حصل عليه تطبيقك عندما اختار المستخدم ملفًا بواسطة WebToApkFS.pickFile.

يُرجع: (متزامن) true عند تحرير الإذن، وfalse خلاف ذلك (لا إذن، أو عنوان خاطئ، أو خارج التطبيق). المتطلبات: فعّل Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.

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 دالة مساعدة (helper)#

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) في الخطوة 4 (الوصول) عند الإنشاء.

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 دالة مساعدة (helper)#

WebToApkFS.writeAll(pathOrUri, base64)

مثال

يستبدل المحتوى الكامل لملف ببايتاتك. يقسّم البيانات إلى أجزاء من 4 ميغابايت نيابةً عنك: الجزء الأول يقتطع الملف، والبقية تُرقَّع بعده.

يُرجع: (متزامن) {ok:true, bytesWritten}، أو {ok:false, error} الخاص بأول جزء فشل. المتطلبات: فعّل Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.

احفظ نص 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 دالة مساعدة (helper)#

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

مثال

يكتب بايتات حقيقية في ملف ويخبرك بعدد ما وصل منها. يمكنه ترقيع بايتات في المنتصف، أو استبدال الملف كله، أو الإضافة إلى النهاية.

يُرجع: (متزامن) {ok, bytesWritten, offset, size} - size تُقرأ من الملف بعد تفريغ البيانات إلى القرص - أو {ok:false, error}. المتطلبات: فعّل Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.

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 دالة مساعدة (helper)#

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) في الخطوة 4 (الوصول) عند الإنشاء.

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: من تلقاء نفسه؛ أما التطبيق فيلتقطها ويحفظ الملف في مجلد التنزيلات في الهاتف باسم الملف الذي تحدده، ثم يعرض "تم تنزيل <الاسم> · فتح" وإشعار "اكتمل التنزيل".

يُرجع: لا شيء للصفحة. المتطلبات: لا شيء للتفعيل. التنزيلات من عناوين الويب (https://…) تذهب إلى مجلد التنزيلات أيضًا (ملف 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 لرابط 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، يُحفظ في التنزيلات بدلًا من ذلك. لتدع المستخدم يختار المجلد استخدم 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 (الكاميرا) فعّل الكاميرا في الخطوة 4 (الوصول) عند الإنشاء؛ ويطلب التطبيق إذن الكاميرا في المرة الأولى.

الصور - يفتح أداة اختيار الصور في 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 (مع تفعيل الكاميرا)الكاميرا مباشرة - صورة لـ image/*، وفيديو لـ video/*
accept="image/*,.pdf" أو بدون acceptأداة اختيار الملفات في النظام

ملاحظات: بدون تفعيل الكاميرا يُتجاهل capture وتُفتح أداة الاختيار. وعند تفعيل Native Folder Access (SAF) يعرض التطبيق قائمته الخاصة "اختيار مصدر الرفع": التقاط صورة / تسجيل فيديو (إذا فُعّلت الكاميرا)، واختيار ملفات، ورفع مجلد (يرسل كل ملف في مجلد). ومع تفعيل التخزين على Android 12 وما قبله، قد يطلب اختيار ملف غير صورة إذن التخزين أولًا. الهواتف التي بلا أداة اختيار الصور تحصل على أداة اختيار الملفات، وتعرض الصور نفسها.

showOpenFilePicker معيار الويب#

showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access

مثال

تعمل واجهة File System Access القياسية داخل التطبيق، فوق أداة اختيار المستندات في 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);
});

اختر عدة صور دفعة واحدة، بدءًا من مجلد الصور:

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() نموذج ملف التبديل في المواصفات - abort() (أو ذهاب الصفحة) يترك الأصل دون مساس؛ و{ keepExistingData: true } يبدأ من المحتوى الحالي؛ وwrite({type:'seek'|'truncate'|'write', …}) وseek() وtruncate() تعمل. الملف الذي يعرضه تطبيقه للقراءة فقط يُجيب بـ queryPermission({mode:'readwrite'}) مع 'denied'، و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 الجسر (bridge)#

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 = التنزيلات، open = عرضه، share، copy). المتطلبات: لا شيء.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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 = التنزيلات، open = عرضه، share، copy). المتطلبات: لا شيء.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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 = التنزيلات، open = عرضه، share، copy). المتطلبات: لا شيء.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

ينهي التنزيل [token] ويتصرف حياله: [action] "save" (التنزيلات، مع شريط "تم التنزيل · فتح" والإشعار)، أو "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 = التنزيلات، open = عرضه، share، copy). المتطلبات: لا شيء.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

getFileHandle / getDirectoryHandle (مع `create`).

مثال

ناقل داخلي خلف getFileHandle(name, {create}) وgetDirectoryHandle(name, {create}). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - دوال __ قد تتغير دون إشعار.

يُرجع: JSON {ok, kind, name, uri, canWrite} أو {ok:false, error} (NotFoundError، TypeMismatchError، NoModificationAllowedError). المتطلبات: مجلد من showDirectoryPicker().

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

getFile(): `{ok, name, type, size, lastModified, url}` (يتدفق المحتوى من url).

مثال

ناقل داخلي يقف خلف FileSystemFileHandle.getFile(). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرةً أبدًا - فقد تتغير دوال __ دون إشعار.

يُرجع: JSON {ok, name, type, size, lastModified, url} - يُبثّ المحتوى من url (أصل التطبيق نفسه، مع النطاقات) - أو {ok:false, error} (NotFoundError، TypeMismatchError، foreign_origin). المتطلبات: مؤشر من إحدى أدوات الاختيار.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

عناصر مؤشر مجلد: `{ok, entries:[{kind, name, uri, canWrite}]}`.

مثال

ناقل داخلي خلف التكرار على FileSystemDirectoryHandle (entries()، keys()، values()، for await). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - دوال __ قد تتغير دون إشعار.

يُرجع: JSON {ok, entries:[{kind, name, uri, canWrite}]} أو {ok:false, error}. المتطلبات: مجلد من showDirectoryPicker().

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

queryPermission() / requestPermission(): `{ok, state:'granted'|'prompt'}` لمؤشر جذر اختياره [rootUri] - 'granted' ما دام التطبيق يحتفظ بإذن ذلك الاختيار، عبر عمليات إعادة التشغيل (الإذن محفوظ؛ ويعود المؤشر من IndexedDB).

مثال

ناقل داخلي خلف FileSystemHandle.queryPermission() وrequestPermission(). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - دوال __ قد تتغير دون إشعار.

يُرجع: JSON {ok:true, state:'granted'} ما دام التطبيق يحتفظ بإذن المجلد أو الملف المختار الذي جاء منه المؤشر (محفوظ عبر عمليات إعادة التشغيل)، و{ok:true, state:'prompt'} بمجرد زواله، أو {ok:false, error:'foreign_origin'}. الملف للقراءة فقط يُجيب بـ 'denied' عن {mode:'readwrite'} في الصفحة دون استدعاء. المتطلبات: مؤشر من إحدى أدوات الاختيار، أو مؤشر أُعيدت قراءته من IndexedDB.

استخدم الواجهة العامة:

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). راجع showOpenFilePicker للاطلاع على واجهة File System Access بالكامل.

__fsPick الجسر (bridge)#

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

يفتح أداة اختيار [kind] (open | save | directory)؛ تصل المؤشرات كـ `appmint:fs`.

مثال

ناقل داخلي خلف showOpenFilePicker() وshowSaveFilePicker() وshowDirectoryPicker(). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - دوال __ قد تتغير دون إشعار.

يُرجع: {ok:true} بمجرد ظهور أداة الاختيار على الشاشة، أو {ok:false, error} (NotAllowedError عند وجود أداة اختيار مفتوحة، foreign_origin). تصل المؤشرات كحدث appmint:fs {requestId, handles:[{kind, name, uri, canWrite}]} أو {requestId, error:'cancelled'}. المتطلبات: لا شيء للتفعيل.

استخدم الواجهة العامة:

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

دع المستخدم يختار عدة صور دفعة واحدة، بدءًا من مجلد الصور:

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 الجسر (bridge)#

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

removeEntry(name, {recursive}).

مثال

ناقل داخلي يقف خلف FileSystemDirectoryHandle.removeEntry(name, {recursive}). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرةً أبدًا - فقد تتغير دوال __ دون إشعار.

يُرجع: JSON {ok:true} أو {ok:false, error} (NotFoundError، InvalidModificationError لمجلد غير فارغ بدون recursive). المتطلبات: مجلد من showDirectoryPicker().

استخدم الواجهة العامة:

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 الجسر (bridge)#

window.WebToApk.__fsWritableAbort(token: String)

abort(): يُسقَط التبديل؛ ويبقى المستند دون مساس.

مثال

ناقل داخلي خلف FileSystemWritableFileStream.abort() - تُسقط نسخة التبديل ويبقى الملف كما كان. تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - دوال __ قد تتغير دون إشعار.

يُرجع: لا شيء. المتطلبات: تدفق كتابة مفتوح.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

close(): يحلّ التبديل محل المستند.

مثال

ناقل داخلي خلف FileSystemWritableFileStream.close() - نسخة التبديل تحلّ محل الملف. تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - دوال __ قد تتغير دون إشعار.

القيمة المُرجَعة: JSON {ok:true} أو {ok:false, error} (NoModificationAllowedError وInvalidModificationError عندما لا يستطيع تطبيق الملف تقصيره). المتطلبات: تيار كتابة مفتوح.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

createWritable(): ملف تبديل، منسوخ من الملف عند [keepExisting].

مثال

ناقل داخلي يقف خلف FileSystemFileHandle.createWritable({keepExistingData}). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرةً أبدًا - فقد تتغير دوال __ دون إشعار.

يُرجع: JSON {ok, token} لنسخة تبديل خاصة، أو {ok:false, error} (NoModificationAllowedError لملف للقراءة فقط). المتطلبات: مؤشر من إحدى أدوات الاختيار.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

truncate(size) على تدفق قابل للكتابة.

مثال

ناقل داخلي يقف خلف FileSystemWritableFileStream.truncate(size). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرةً أبدًا - فقد تتغير دوال __ دون إشعار.

القيمة المُرجَعة: JSON {ok:true} أو {ok:false, error}. المتطلبات: تيار كتابة مفتوح.

استخدم الواجهة العامة:

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 الجسر (bridge)#

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

جزء واحد من write() لتدفق قابل للكتابة، عند [position].

مثال

ناقل داخلي خلف FileSystemWritableFileStream.write() - جزء واحد عند موضع في نسخة التبديل. تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - دوال __ قد تتغير دون إشعار.

القيمة المُرجَعة: JSON {ok:true} أو {ok:false, error}. المتطلبات: تيار كتابة مفتوح.

استخدم الواجهة العامة:

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 الجسر (bridge)#

window.WebToApk.getMaxIoChunkSize(): Long

الحد الأقصى للبايتات لكل استدعاء لـ [readFileBase64] / [writeFileBase64]، لحساب الأجزاء.

مثال

الحد الأقصى للبايتات لكل استدعاء لـ readFileBase64 / writeFileBase64 (4 ميغابايت). تستخدم الصفحات عادةً WebToApkFS.maxChunkSize().

يُرجع: (متزامن) رقمًا (4194304). المتطلبات: لا شيء لاستدعائه؛ أما دوال القراءة/الكتابة التي يصفها فتحتاج تفعيل Native Folder Access (SAF) في الخطوة 4 (الوصول).

الطريقة العامة:

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 الجسر (bridge)#

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 الكتلية تتداخل.)

مثال

يفتح منتقي النظام لملف **واحد**. تستخدم الصفحات عادةً مساعد Promise WebToApkFS.pickFile(mimeFilter)؛ وهذه الدالة الخام تجيب عبر استدعاء.

يُرجع: لا شيء الآن. لاحقًا يستدعي الغلاف 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) في الخطوة 4 (الوصول) عند الإنشاء.

الطريقة العامة (موصى بها):

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 الجسر (bridge)#

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

يقرأ حتى [length] من البايتات الخام ابتداءً من [offset] ويعيدها بصيغة base64. @param pathOrUri عنوان `content://` من [pickFile]، أو مسار نسبي إلى المجلد الممنوح عبر [requestFolderAccess]. @param length عدد البايتات المقروءة؛ يُحصر بـ 4 ميغابايت لكل استدعاء. تعني 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) في الخطوة 4 (الوصول) عند الإنشاء.

الطريقة العامة:

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 الجسر (bridge)#

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

يُحرّر إذن ملف واحد محفوظ أُخذ بواسطة [pickFile]. يحدّ Android عدد أذونات العناوين التي يمكن لتطبيق حفظها، فعلى التطبيق الذي يختار ملفات كثيرة أن يحرر ما انتهى منه.

مثال

يُحرّر إذن القراءة/الكتابة المحفوظ لملف اختير باستخدام pickFile. تستخدم الصفحات عادةً WebToApkFS.releaseFile(uri)، الذي يُرجع قيمة منطقية حقيقية.

يُرجع: (متزامن) السلسلة "true" أو "false". المتطلبات: فعّل Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء (وإلا فهي دائمًا "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 الجسر (bridge)#

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) في الخطوة 4 (الوصول) عند الإنشاء.

الطريقة العامة:

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 الجسر (bridge)#

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، فيستطيع المستدعي التحقق من الكتابة فعليًا بدل الوثوق بقيمة منطقية.

مثال

يكتب بايتات 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) في الخطوة 4 (الوصول) عند الإنشاء.

الطريقة العامة:

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 ميغابايت من البايتات المفكوكة (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.

يتم إنشاؤه من بيئة تشغيل التطبيق وملفات الأمثلة الخاصة به عند كل بناء للمستندات. اقرأه بصيغة Markdown · جميع العائلات.

تمت المراجعة مقابل الجسر المُرفق بتاريخ 2026-10-22.