واجهة JavaScript bridge API
الملفات: مجلد منحه المستخدم
مجلد يُختار مرة واحدة، ويُقرأ ويُكتب فيه بعد ذلك، بإذن يبقى بعد إعادة التشغيل.
WebToApkFS.clearAccess دالة مساعدة (helper)#
WebToApkFS.clearAccess()
مثال
ينسى المجلد الممنوح ويعيد الإذن إلى Android.
القيمة المُرجَعة: لا شيء (متزامن). المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.
قدّم دائمًا طريقة لقطع الاتصال بجانب اسم المجلد:
document.getElementById('disconnect').addEventListener('click', function () {
if (!window.WebToApkFS) return;
window.WebToApkFS.clearAccess();
console.log('still connected?', window.WebToApkFS.hasAccess()); // false
document.getElementById('folder-name').textContent = 'No folder chosen';
document.getElementById('choose-folder').hidden = false;
});زر واحد يوصل أو يقطع الاتصال، بحسب الحالة الحالية:
var btn = document.getElementById('folder-toggle');
function paint() {
btn.textContent = window.WebToApkFS.hasAccess() ? 'Disconnect folder' : 'Connect a folder';
}
btn.addEventListener('click', async function () {
if (!window.WebToApkFS) return;
if (window.WebToApkFS.hasAccess()) {
if (!confirm('Stop saving to this folder? Your files stay where they are.')) return;
window.WebToApkFS.clearAccess();
} else {
await window.WebToApkFS.requestAccess();
}
paint();
});
if (window.WebToApkFS) paint();بدّل الحسابات: انسَ المجلد مع بقية حالة تسجيل الدخول:
function switchAccount() {
sessionStorage.clear();
if (window.WebToApkFS) window.WebToApkFS.clearAccess(); // the next account picks its own folder
showLoginScreen();
}ملاحظات: تبقى الملفات حيث هي؛ لكن التطبيق لا يستطيع الوصول إليها بعد الآن. لاستخدام مجلد مرة أخرى، استدعِ WebToApkFS.requestAccess() من نقرة.
WebToApkFS.delete دالة مساعدة (helper)#
WebToApkFS.delete(relativePath, options)
مثال
يحذف ملفًا أو مجلدًا داخل المجلد الممنوح.
القيمة المُرجَعة: قيمة منطقية، بشكل متزامن: true عند الحذف. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء، ومجلد ممنوح.
احذف ملفًا واحدًا، بعد السؤال:
if (window.WebToApkFS && confirm('Delete notes/old.txt?')) {
var ok = window.WebToApkFS.delete('notes/old.txt');
if (!ok) alert('Could not delete the file.');
}احذف مجلدًا وكل ما بداخله - مرّر { recursive: true }:
if (window.WebToApkFS && confirm('Delete the whole 2025 folder?')) {
var gone = window.WebToApkFS.delete('notes/2025', { recursive: true });
console.log(gone ? 'deleted' : 'not deleted');
}نظّف الصادرات الأقدم من 30 يومًا، ملفًا ملفًا:
function pruneOldExports() {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return 0;
var cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
var removed = 0;
window.WebToApkFS.list('exports').forEach(function (e) {
if (e.isDirectory || e.lastModified >= cutoff) return;
if (window.WebToApkFS.delete('exports/' + e.name)) removed++;
});
return removed;
}
if (confirm('Delete exports older than 30 days?')) {
document.getElementById('status').textContent = pruneOldExports() + ' old exports deleted';
}ملاحظات: بدون { recursive: true } لا يُحذف المجلد إلا إذا كان فارغًا. أما المجلد الممنوح نفسه ('') فلا يمكن حذفه أبدًا. الحذف فوري ولا توجد سلة مهملات، لذا أكّد مع المستخدم دائمًا أولاً.
WebToApkFS.getAccessUri دالة مساعدة (helper)#
WebToApkFS.getAccessUri()
مثال
يعطي عنوان المجلد الممنوح، لتعرض للمستخدم أين تذهب الملفات.
القيمة المُرجَعة: نص، بشكل متزامن: عنوان شجرة مثل content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes، أو '' عندما لا يوجد مجلد ممنوح. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.
اعرض اسم مجلد مقروءًا بجانب زر قطع الاتصال:
function connectedFolderName() {
if (!window.WebToApkFS) return '';
var uri = window.WebToApkFS.getAccessUri();
if (!uri) return '';
var tail = uri.split('/tree/')[1] || '';
try { tail = decodeURIComponent(tail); } catch (e) {}
return tail.split(':').pop() || tail; // "Documents/Notes"
}
var name = connectedFolderName();
document.getElementById('folder-name').textContent = name ? 'Folder: ' + name : 'No folder chosen';لاحظ أن المستخدم اختار مجلدًا مختلفًا منذ الزيارة الأخيرة، وأعد بناء ما خزّنته عن المجلد القديم:
function checkFolderChanged() {
if (!window.WebToApkFS) return;
var uri = window.WebToApkFS.getAccessUri();
var last = '';
try { last = localStorage.getItem('folderUri') || ''; } catch (e) {}
if (uri && uri !== last) {
rebuildFileIndex(); // your app: re-read the new folder
try { localStorage.setItem('folderUri', uri); } catch (e) {}
}
}
checkFolderChanged();زر قطع اتصال ينسى المجلد ويؤكد أنه اختفى:
document.getElementById('disconnect').addEventListener('click', function () {
if (!window.WebToApkFS || !window.WebToApkFS.getAccessUri()) return;
if (!confirm('Stop saving to this folder? Your files stay where they are.')) return;
window.WebToApkFS.clearAccess();
var still = window.WebToApkFS.getAccessUri();
document.getElementById('folder-name').textContent = still ? 'Could not disconnect' : 'No folder chosen';
});ملاحظات: لا تأخذ دوال الملفات هذا الـ URI - بل تأخذ مسارات نسبية إلى المجلد الممنوح ('notes/today.txt'). صيغة الـ URI تخص مزوّد الملفات الذي اختار منه المستخدم، لذا استخدمه للعرض فقط.
WebToApkFS.hasAccess دالة مساعدة (helper)#
WebToApkFS.hasAccess()
مثال
يخبرك هل أعطى المستخدم التطبيق مجلدًا بالفعل.
القيمة المُرجَعة: قيمة منطقية، بشكل متزامن. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.
اعرض "اختر مجلدًا" أو قائمة الملفات، بحسب الإجابة:
function renderFolderScreen() {
if (!window.WebToApkFS) {
document.getElementById('folder-panel').hidden = true; // not in the app, or not enabled
return;
}
var connected = window.WebToApkFS.hasAccess();
document.getElementById('choose-folder').hidden = connected;
document.getElementById('file-list').hidden = !connected;
if (connected) {
var entries = window.WebToApkFS.list('');
document.getElementById('file-list').textContent =
entries.map(function (e) { return e.name; }).join('\n');
}
}احفظ تلقائيًا فقط أثناء اتصال مجلد، وقل ذلك عندما لا يكون متصلاً:
var saveTimer = null;
document.getElementById('editor').addEventListener('input', function () {
clearTimeout(saveTimer);
saveTimer = setTimeout(function () {
var status = document.getElementById('status');
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) {
status.textContent = 'Choose a folder to turn on autosave';
return;
}
var ok = window.WebToApkFS.writeText('notes/draft.txt', document.getElementById('editor').value);
status.textContent = ok ? 'Saved' : 'Could not save';
}, 800);
});عند البدء، أعد فتح آخر ملاحظة عندما يكون مجلد متذكَّرًا، وإلا اعرض شاشة الترحيب:
window.addEventListener('load', function () {
var fs = window.WebToApkFS;
if (!fs || !fs.hasAccess()) {
showWelcome(); // your app: explains "Choose folder"
return;
}
var last = 'notes/today.txt';
try { last = localStorage.getItem('lastNote') || last; } catch (e) {}
document.getElementById('editor').value = fs.readText(last);
});ملاحظات: يقول فقط إن التطبيق يتذكر مجلدًا. إذا حذف المستخدم ذلك المجلد أو نقله، يُرجع list('') القيمة [] - اعرض "اختر مجلدًا" مرة أخرى. يظهر window.WebToApkFS عند انتهاء تحميل الصفحة؛ وأثناء التحميل استخدم الدالة الخام window.WebToApk.hasFolderAccess() === 'true'.
WebToApkFS.isAvailable دالة مساعدة (helper)#
WebToApkFS.isAvailable()
مثال
يخبرك هل يستطيع هذا التطبيق استخدام مجلد منحه المستخدم أصلاً.
القيمة المُرجَعة: قيمة منطقية، بشكل متزامن: true عندما يكون الوصول إلى المجلدات مفعّلاً في الإنشاء. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء - بدونه (وبدون المفاتيح الآمنة) لا يُضاف window.WebToApkFS إلى الصفحة؛ ومع المفاتيح الآمنة فقط يكون موجودًا ويجيب false.
window.WebToApkFS يُضاف قبل تشغيل أول سكربت في صفحتك؛ وعلى WebViews الأقدم من 2021 تقريبًا يصل فقط عند انتهاء تحميل الصفحة. لتغطية تلك أيضًا، انتظره قليلاً قبل أن تقرر:
function whenFolderHelperReady(done) {
var tries = 0;
(function check() {
if (window.WebToApkFS) return done(window.WebToApkFS);
if (++tries > 30) return done(null); // in a browser, or the build did not enable it
setTimeout(check, 100);
})();
}
whenFolderHelperReady(function (fs) {
var usable = !!(fs && fs.isAvailable());
document.getElementById('folder-panel').hidden = !usable;
});داخل نقرة زر تكون الصفحة قد انتهت من التحميل منذ وقت طويل، لذا يكفي فحص مباشر:
if (window.WebToApkFS && window.WebToApkFS.isAvailable()) {
// show the folder features
}حوّل الفحصين إلى حالة واحدة تقود شاشة المجلد كلها:
function folderState() {
var fs = window.WebToApkFS;
if (!fs || !fs.isAvailable()) return 'unsupported'; // this build has no folder access
return fs.hasAccess() ? 'ready' : 'needs-folder';
}
var state = folderState();
document.getElementById('folder-panel').hidden = state === 'unsupported';
document.getElementById('choose-folder').hidden = state !== 'needs-folder';
document.getElementById('export').disabled = state !== 'ready';ملاحظات: هذا لا يخبر هل اختير مجلد بعد - استخدم WebToApkFS.hasAccess() لذلك. قبل تحميل الصفحة، يجيب window.WebToApk.isFolderAccessEnabled() الخام (النص "true"/"false") عن السؤال نفسه.
WebToApkFS.list دالة مساعدة (helper)#
WebToApkFS.list(relativePath)
مثال
يسرد الملفات والمجلدات الفرعية داخل مجلد من المجلد الممنوح.
القيمة المُرجَعة: مصفوفة، بشكل متزامن: [{ name, uri, isDirectory, length, lastModified }] (length بالبايت، lastModified بالمللي ثانية منذ 1970). المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء، ومجلد ممنوح.
متصفح ملفات بسيط ('' هو المجلد الممنوح نفسه):
function renderFolder(path) {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return;
var ul = document.getElementById('files');
ul.innerHTML = '';
window.WebToApkFS.list(path)
.sort(function (a, b) { return (b.isDirectory - a.isDirectory) || a.name.localeCompare(b.name); })
.forEach(function (e) {
var li = document.createElement('li');
li.textContent = (e.isDirectory ? '[folder] ' : '') + e.name +
(e.isDirectory ? '' : ' (' + e.length + ' bytes, ' + new Date(e.lastModified).toLocaleDateString() + ')');
if (e.isDirectory) li.onclick = function () { renderFolder(path ? path + '/' + e.name : e.name); };
ul.appendChild(li);
});
}
renderFolder('');مرّ على كل مجلد فرعي لجمع كل ملاحظات Markdown (مسارات يمكنك تمريرها إلى readText):
function findNotes(path, out) {
out = out || [];
window.WebToApkFS.list(path).forEach(function (e) {
var full = path ? path + '/' + e.name : e.name;
if (e.isDirectory) findNotes(full, out);
else if (/\.md$/i.test(e.name)) out.push(full);
});
return out;
}
if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
var notes = findNotes('');
document.getElementById('count').textContent = notes.length + ' notes';
}اعثر على أحدث نسخة احتياطية وعلى المساحة التي تشغلها النسخ الاحتياطية:
function backupSummary() {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return null;
var files = window.WebToApkFS.list('backups').filter(function (e) { return !e.isDirectory; });
if (!files.length) return null;
var newest = files.reduce(function (a, b) { return b.lastModified > a.lastModified ? b : a; });
var bytes = files.reduce(function (sum, e) { return sum + e.length; }, 0);
return { newest: 'backups/' + newest.name, count: files.length, kb: Math.round(bytes / 1024) };
}
var s = backupSummary();
document.getElementById('backup-info').textContent = s
? s.count + ' backups, ' + s.kb + ' KB. Latest: ' + s.newest
: 'No backups yet';ملاحظات: المجلد الفارغ والمجلد غير الموجود ومسار هو ملف ومسار فيه .. وعدم وجود مجلد ممنوح كلها تُرجع []. القائمة غير مرتبة. كل uri يعمل أيضًا مع WebToApkFS.stat() و WebToApkFS.readBytes().
WebToApkFS.mkdir دالة مساعدة (helper)#
WebToApkFS.mkdir(relativePath)
مثال
ينشئ مجلدًا داخل المجلد الممنوح، بما في ذلك أي مجلدات أم ناقصة.
القيمة المُرجَعة: قيمة منطقية، بشكل متزامن: true عندما يكون المجلد موجودًا بعد ذلك. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء، ومجلد ممنوح.
if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
var month = new Date().toISOString().slice(0, 7); // "2026-06"
if (window.WebToApkFS.mkdir('journal/' + month)) {
window.WebToApkFS.writeText('journal/' + month + '/readme.txt', 'One file per day.');
} else {
alert('Could not create the folder.');
}
}جهّز بنية مجلدات مشروع عند أول استخدام، وأبلغ عن أي مجلد تعذر إنشاؤه:
function createProjectFolders(project) {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
var failed = ['images', 'exports', 'drafts'].filter(function (sub) {
return !window.WebToApkFS.mkdir('projects/' + project + '/' + sub);
});
if (failed.length) alert('Could not create: ' + failed.join(', '));
return failed.length === 0;
}
createProjectFolders('holiday-2026');زر "مجلد جديد" يحذّر عندما يكون الاسم مأخوذًا (لأن mkdir يجيب بـ true لمجلد موجود):
document.getElementById('new-folder').addEventListener('click', function () {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return;
var name = (prompt('Folder name') || '').trim();
if (!name || /[\\/]/.test(name) || name === '.' || name === '..') return;
var taken = window.WebToApkFS.list('').some(function (e) { return e.name === name; });
if (taken) { alert('"' + name + '" already exists.'); return; }
if (window.WebToApkFS.mkdir(name)) renderFolder(''); // your app: refresh the list
else alert('Could not create "' + name + '".');
});ملاحظات: يُرجع true إذا كان المجلد موجودًا بالفعل، لذا لا بأس باستدعائه قبل كل حفظ. ويُرجع false عندما **يعترض** ملف بهذا الاسم، أو يحتوي المسار على ..، أو لا يوجد مجلد ممنوح. WebToApkFS.writeText() ينشئ المجلدات الأم الناقصة أصلاً، فلا تحتاج mkdir إلا للمجلدات الفارغة.
WebToApkFS.readText دالة مساعدة (helper)#
WebToApkFS.readText(relativePath)
مثال
يقرأ ملفًا نصيًا من المجلد الممنوح بصيغة UTF-8.
القيمة المُرجَعة: نص، بشكل متزامن: نص الملف، أو ''. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء، ومجلد ممنوح.
'' تعني "ملف فارغ" أو "لا يوجد ملف كهذا". استخدم WebToApkFS.stat() عندما يجب أن تعرف أيهما:
function loadSettings() {
if (!window.WebToApkFS) return null;
var info = window.WebToApkFS.stat('data/settings.json');
if (!info.ok) return null; // not saved yet
try {
return JSON.parse(window.WebToApkFS.readText('data/settings.json'));
} catch (e) {
alert('settings.json is damaged: ' + e.message);
return null;
}
}افتح ملاحظة في مربع نص:
if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
document.getElementById('editor').value = window.WebToApkFS.readText('notes/today.txt');
}استورد ملف CSV وضعه المستخدم في المجلد:
function importCsv(path) {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return [];
var text = window.WebToApkFS.readText(path);
if (!text) return []; // missing or empty
var lines = text.replace(/\r/g, '').split('\n').filter(Boolean);
var header = lines.shift().split(',');
return lines.map(function (line) {
var cells = line.split(',');
var row = {};
header.forEach(function (h, i) { row[h.trim()] = (cells[i] || '').trim(); });
return row;
});
}
var contacts = importCsv('import/contacts.csv');
console.log(contacts.length + ' rows imported');ملاحظات: يُحمَّل الملف كله دفعة واحدة - للملفات الكبيرة أو الثنائية استخدم WebToApkFS.readBytes(). البايتات غير الصالحة كـ UTF-8 تعود برمز الاستبدال. كما أن المجلد أو المسار الذي فيه .. أو عدم وجود مجلد ممنوح يُرجع أيضًا ''.
WebToApkFS.rename دالة مساعدة (helper)#
WebToApkFS.rename(relativePath, newName)
مثال
يعيد تسمية ملف أو مجلد داخل المجلد الممنوح.
القيمة المُرجَعة: قيمة منطقية، بشكل متزامن. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء، ومجلد ممنوح.
newName هو الاسم الجديد فقط، وليس مسارًا. يبقى المدخل في المجلد نفسه:
function renameNote(path, newName) {
if (!window.WebToApkFS) return false;
var ok = window.WebToApkFS.rename(path, newName); // 'notes/a.txt', 'b.txt' -> notes/b.txt
if (!ok) alert('Could not rename. A file called "' + newName + '" may already exist.');
return ok;
}
renameNote('notes/today.txt', 'journal.txt');
renameNote('notes/2025', 'archive-2025'); // folders work tooأعد التسمية دون تعارض: جرّب "name (2)" و"name (3)" … حتى يوجد اسم متاح:
function renameUnique(path, wanted) {
if (!window.WebToApkFS) return '';
var dot = wanted.lastIndexOf('.');
var base = dot > 0 ? wanted.slice(0, dot) : wanted;
var ext = dot > 0 ? wanted.slice(dot) : '';
for (var n = 1; n <= 20; n++) {
var name = n === 1 ? wanted : base + ' (' + n + ')' + ext;
if (window.WebToApkFS.rename(path, name)) return name;
}
return '';
}
var finalName = renameUnique('photos/IMG_0042.jpg', 'Beach.jpg');احفظ بأمان: اكتب ملفًا مؤقتًا أولاً، ثم بدّله (تعيد التسمية ترفض اسمًا مأخوذًا، لذا يُحذف الملف القديم قبلها مباشرة):
function saveSafely(folder, name, text) {
var fs = window.WebToApkFS;
if (!fs || !fs.hasAccess()) return false;
var tmp = folder + '/' + name + '.tmp';
if (!fs.writeText(tmp, text)) return false; // the original is untouched
fs.delete(folder + '/' + name); // false when it did not exist yet
return fs.rename(tmp, name);
}
saveSafely('notes', 'today.txt', document.getElementById('editor').value);ملاحظات: يُرجع false عندما يكون لدى مدخل آخر newName بالفعل، أو عندما تكون newName فارغة أو ./.. أو تحتوي على / أو \، أو عندما لا يوجد المسار، أو عندما لا يوجد مجلد ممنوح. لنقل ملف بين المجلدات، اقرأه واكتبه في المكان الجديد ثم احذف القديم.
WebToApkFS.requestAccess دالة مساعدة (helper)#
WebToApkFS.requestAccess()
مثال
يفتح منتقي مجلدات النظام ليتمكن المستخدم من منح التطبيق مجلدًا واحدًا، يُحتفظ به عبر إعادة التشغيل.
القيمة المُرجَعة: Promise يتحقق دائمًا (لا يرفض أبدًا) بـ { success, folderUri, requestId }، مع error عندما تكون success خاطئة. success قيمة منطقية؛ وfolderUri هو عنوان content://…/tree/… الممنوح، أو ''. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.
اطلب من زر ينقره المستخدم، واعرض أي مجلد متصل:
document.getElementById('choose-folder').addEventListener('click', async function () {
if (!window.WebToApkFS) {
alert('Folder access is not available in this app.');
return;
}
var r = await window.WebToApkFS.requestAccess();
if (!r.success) {
document.getElementById('status').textContent = 'No folder chosen.';
return;
}
var name = decodeURIComponent(r.folderUri.split('/tree/')[1] || '').split(':').pop();
document.getElementById('status').textContent = 'Saving to: ' + name;
window.WebToApkFS.writeText('hello.txt', 'Connected at ' + new Date().toISOString());
});اطلب فقط عندما لا يوجد مجلد ممنوح بعد:
async function ensureFolder() {
if (!window.WebToApkFS) return false;
if (window.WebToApkFS.hasAccess()) return true;
var r = await window.WebToApkFS.requestAccess();
return r.success;
}زر "تغيير المجلد" يشرح كل طريقة قد يفشل بها الطلب:
var folderErrors = {
cancelled: 'No folder chosen.',
busy: 'The folder picker is already open.',
not_persisted: 'Android would not keep access to that folder. Try another one.',
disabled: 'Folder access is switched off in this app.',
no_picker: 'This phone has no folder picker.',
foreign_origin: 'This page is not allowed to use folders.'
};
document.getElementById('change-folder').addEventListener('click', async function () {
if (!window.WebToApkFS) return;
var r = await window.WebToApkFS.requestAccess();
document.getElementById('status').textContent = r.success
? 'Folder changed.'
: (folderErrors[r.error] || 'Could not open the folder picker.');
if (r.success) renderFolder(''); // your app: show the new folder
});ملاحظات: استدعِه من نقرة، وليس عند تحميل الصفحة - فالمنتقي يغطي التطبيق. يأتي success: false مع error: 'cancelled' (تراجع المستخدم)، 'not_persisted' (لم يحتفظ Android بالمنح)، 'disabled'، 'busy' (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته)، 'no_picker' أو 'foreign_origin'. الاختيار مرة أخرى يستبدل المجلد السابق.
WebToApkFS.writeText دالة مساعدة (helper)#
WebToApkFS.writeText(relativePath, content)
مثال
يكتب ملفًا نصيًا UTF-8 في المجلد الممنوح، فيُنشئه (مع المجلدات الأم الناقصة) أو يستبدله.
القيمة المُرجَعة: قيمة منطقية، بشكل متزامن: true عند كتابة الملف. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء، ومجلد ممنوح.
احفظ من زر:
document.getElementById('save').addEventListener('click', async function () {
if (!window.WebToApkFS) { alert('Saving to a folder is not available here.'); return; }
if (!window.WebToApkFS.hasAccess()) {
var r = await window.WebToApkFS.requestAccess();
if (!r.success) return; // user cancelled
}
var ok = window.WebToApkFS.writeText('notes/today.txt', document.getElementById('editor').value);
document.getElementById('status').textContent = ok ? 'Saved' : 'Could not save';
});احفظ كائنًا بصيغة JSON:
if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
window.WebToApkFS.writeText('data/settings.json', JSON.stringify({ theme: 'dark', size: 16 }, null, 2));
}أضف سطرًا إلى سجل يومي (لا يوجد append، لذا اقرأ وأضف واكتب مرة أخرى):
function logLine(message) {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
var path = 'logs/' + new Date().toISOString().slice(0, 10) + '.log'; // one file per day
var old = window.WebToApkFS.readText(path);
var line = new Date().toLocaleTimeString() + ' ' + message + '\n';
return window.WebToApkFS.writeText(path, old + line);
}
logLine('Order #1042 marked as shipped');ملاحظات: يُستبدل الملف الموجود بالكامل - لا يوجد إلحاق. يُحوَّل content إلى نص (null تصبح ''). يُرجع false عندما يكون المسار مجلدًا موجودًا، أو يحتوي على ..، أو لا يوجد مجلد ممنوح. يُؤخذ نوع الملف من الامتداد، فالاسم على القرص هو تمامًا الاسم الذي أعطيته.
clearFolderAccess الجسر (bridge)#
window.WebToApk.clearFolderAccess()
يحرر منح المجلد المحفوظ. يُرفض (دون تأثير) لصفحة غريبة.
مثال
ينسى المجلد الممنوح ويعيد الإذن إلى Android.
القيمة المُرجَعة: لا شيء (متزامن). وتفعل WebToApkFS.clearAccess() الشيء نفسه. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول)؛ ومع إيقاف المفتاح لا يفعل شيئًا.
زر "قطع الاتصال":
document.getElementById('disconnect').addEventListener('click', function () {
if (window.WebToApkFS) {
window.WebToApkFS.clearAccess();
} else if (window.WebToApk && typeof window.WebToApk.clearFolderAccess === 'function') {
window.WebToApk.clearFolderAccess();
}
document.getElementById('folder-name').textContent = 'No folder chosen';
});عند تسجيل الخروج، انسَ المجلد أيضًا، ليختار الشخص التالي على هذا الهاتف مجلده:
function signOut() {
localStorage.clear();
if (window.WebToApk && typeof window.WebToApk.clearFolderAccess === 'function') {
window.WebToApk.clearFolderAccess();
}
location.href = 'login.html';
}انسَ مجلدًا نُقل أو حُذف في تطبيق الملفات، ثم اعرض الاختيار من جديد (يكتب التطبيق myapp-folder.json عند وصل مجلد لأول مرة):
async function reconnectIfMissing() {
var fs = window.WebToApkFS;
if (!fs || !fs.hasAccess()) return;
if (fs.stat('myapp-folder.json').ok) return; // marker still there: the folder is fine
if (typeof window.WebToApk.clearFolderAccess === 'function') {
window.WebToApk.clearFolderAccess(); // drop the grant that points nowhere
}
if (confirm('The folder you chose earlier is gone. Choose a folder again?')) {
var r = await fs.requestAccess();
if (r.success) fs.writeText('myapp-folder.json', '{}');
}
}ملاحظات: لا تُحذف الملفات في المجلد؛ لكن التطبيق لا يستطيع الوصول إليها بعد الآن. بعد ذلك، يجيب hasFolderAccess() بـ "false" وتفشل كل دوال الملفات حتى يختار المستخدم مجلدًا مرة أخرى.
deleteEntry الجسر (bridge)#
window.WebToApk.deleteEntry(relativePath: String, recursive: String): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
يحذف ملفًا أو مجلدًا داخل المجلد الممنوح.
القيمة المُرجَعة: النص "true" أو "false"، بشكل متزامن. وتُرجع WebToApkFS.delete(path, { recursive }) قيمة منطقية حقيقية. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول) ومجلد ممنوح.
باستخدام المساعد، بعد سؤال المستخدم:
function removeEntry(path, isFolder) {
if (!window.WebToApkFS) return false;
if (!confirm('Delete ' + path + '? This cannot be undone.')) return false;
return window.WebToApkFS.delete(path, { recursive: isFolder });
}
removeEntry('notes/old.txt', false);
removeEntry('notes/2025', true); // the folder and everything in itالاستدعاء الخام - الوسيط الثاني هو النص 'true' أو 'false':
if (window.WebToApk && typeof window.WebToApk.deleteEntry === 'function') {
var gone = window.WebToApk.deleteEntry('cache', 'true') === 'true';
}احتفظ بأحدث خمس نسخ احتياطية فقط واحذف الباقي:
function pruneBackups(keep) {
var fs = window.WebToApkFS;
if (!fs) return 0;
var files = fs.list('backups').filter(function (e) { return !e.isDirectory; });
files.sort(function (a, b) { return b.lastModified - a.lastModified; }); // newest first
var removed = 0;
files.slice(keep).forEach(function (e) {
if (fs.delete('backups/' + e.name)) removed++;
});
return removed;
}
console.log(pruneBackups(5) + ' old backups deleted');ملاحظات: المجلد غير الفارغ لا يُحذف إلا مع 'true' التكراري. أما المجلد الممنوح نفسه ('') فلا يمكن حذفه أبدًا. الحذف حقيقي وفوري: لا توجد سلة مهملات.
getFolderAccessUri الجسر (bridge)#
window.WebToApk.getFolderAccessUri(): String
عنوان شجرة `content://` للمجلد الممنوح، أو "" (لا شيء، أو معطّل، أو صفحة غريبة).
مثال
يعطي عنوان المجلد الذي منحه المستخدم، لتعرض أي مجلد متصل.
القيمة المُرجَعة: نص، بشكل متزامن: عنوان شجرة مثل content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes، أو "" عندما لا يوجد مجلد ممنوح أو يكون المفتاح متوقفًا. وتُرجع WebToApkFS.getAccessUri() النص نفسه. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول).
حوّل الـ URI إلى اسم يتعرف عليه المستخدم:
function folderLabel() {
var uri = '';
if (window.WebToApkFS) {
uri = window.WebToApkFS.getAccessUri();
} else if (window.WebToApk && typeof window.WebToApk.getFolderAccessUri === 'function') {
uri = window.WebToApk.getFolderAccessUri();
}
if (!uri) return '';
var tail = uri.split('/tree/')[1] || '';
try { tail = decodeURIComponent(tail); } catch (e) {}
return tail.split(':').pop() || tail; // "primary:Documents/Notes" -> "Documents/Notes"
}
var label = folderLabel();
document.getElementById('folder-name').textContent = label ? 'Saving to: ' + label : 'No folder chosen';أعد تحميل قائمة الملفات فقط عندما يختار المستخدم فعلاً مجلدًا مختلفًا:
document.getElementById('change-folder').addEventListener('click', async function () {
if (!window.WebToApkFS) return;
var before = window.WebToApkFS.getAccessUri();
var r = await window.WebToApkFS.requestAccess();
if (!r.success) return; // the old folder is still connected
if (r.folderUri !== before) {
renderFiles(window.WebToApkFS.list('')); // a new place: show what is in it
}
});قل هل المجلد على الهاتف أم على بطاقة SD أم في تطبيق تخزين آخر:
function folderLocation() {
var uri = window.WebToApkFS ? window.WebToApkFS.getAccessUri() : '';
if (!uri) return 'none';
var id = uri.split('/tree/')[1] || '';
try { id = decodeURIComponent(id); } catch (e) {}
if (uri.indexOf('com.android.externalstorage.documents') === -1) return 'other'; // e.g. a cloud drive app
return id.indexOf('primary:') === 0 ? 'phone' : 'sd-card'; // "1A2B-3C4D:Music" is a card
}
document.getElementById('folder-where').textContent =
{ none: '', phone: 'Phone storage', 'sd-card': 'SD card', other: 'Another storage app' }[folderLocation()];ملاحظات: لا تمرّر هذا الـ URI مرة أخرى إلى دوال الملفات - كلها تأخذ مسارات نسبية إلى هذا المجلد. اعرض الاسم بجانب زر "قطع الاتصال" ليعرف المستخدم أين تذهب الملفات.
hasFolderAccess الجسر (bridge)#
window.WebToApk.hasFolderAccess(): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
يخبرك هل أعطى المستخدم التطبيق مجلدًا بالفعل.
القيمة المُرجَعة: النص "true" أو "false" (وليس قيمة منطقية)، بشكل متزامن. وتعطي WebToApkFS.hasAccess() قيمة منطقية حقيقية. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول)؛ وبدونه تكون الإجابة دائمًا "false".
الدالة الخام موجودة منذ السطر الأول من الصفحة، فهي الفحص المناسب أثناء تحميل الصفحة (ويُضاف WebToApkFS فقط عند انتهاء التحميل):
function folderConnected() {
if (window.WebToApkFS) return window.WebToApkFS.hasAccess();
return !!(window.WebToApk && typeof window.WebToApk.hasFolderAccess === 'function' &&
window.WebToApk.hasFolderAccess() === 'true');
}
document.getElementById('choose-folder').hidden = folderConnected();افحص قبل الحفظ وافتح المنتقي فقط عندما لا يكون هناك مجلد متصل بعد:
async function saveBackup(json) {
if (!window.WebToApkFS) return false;
if (!window.WebToApkFS.hasAccess()) {
var r = await window.WebToApkFS.requestAccess(); // asked once; the grant is remembered
if (!r.success) return false;
}
return window.WebToApkFS.writeText('backup.json', json);
}
document.getElementById('backup').addEventListener('click', function () {
saveBackup(JSON.stringify(appState));
});مؤقّت حفظ تلقائي يكتب فقط عندما يكون مجلد متصلاً ولا يفتح المنتقي بنفسه أبدًا:
setInterval(function () {
if (!window.WebToApk || typeof window.WebToApk.hasFolderAccess !== 'function') return;
if (window.WebToApk.hasFolderAccess() !== 'true') return; // not connected: the Save button asks
window.WebToApk.writeTextFile('drafts/autosave.txt', document.getElementById('editor').value);
}, 30000);ملاحظات: قارن بـ === 'true'؛ فالنص "false" يُعدّ صحيحًا (truthy). وهو يقول فقط إن التطبيق يتذكر مجلدًا. إذا حُذف ذلك المجلد أو نُقل في تطبيق الملفات، يجيب listFolderEntries('') بـ [] - اطلب من المستخدم اختياره مرة أخرى.
isFolderAccessEnabled الجسر (bridge)#
window.WebToApk.isFolderAccessEnabled(): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
يخبرك هل بُني هذا التطبيق أصلاً مع تفعيل الوصول إلى المجلدات.
القيمة المُرجَعة: النص "true" أو "false"، بشكل متزامن. المتطلبات: لا شيء لاستدعائه؛ ولا يجيب بـ "true" إلا عندما حُدّد Native Folder Access (SAF) في الخطوة 4 (الوصول).
استخدمه لتقرر هل تعرض ميزة المجلدات. وتجيب WebToApkFS.isAvailable() عن السؤال نفسه كقيمة منطقية بعد تحميل الصفحة:
function folderFeatureShipped() {
if (window.WebToApkFS) return window.WebToApkFS.isAvailable();
return !!(window.WebToApk && typeof window.WebToApk.isFolderAccessEnabled === 'function' &&
window.WebToApk.isFolderAccessEnabled() === 'true');
}
if (!folderFeatureShipped()) {
document.getElementById('folder-panel').hidden = true; // in a browser, or the switch was off
}قُد صفًا في الإعدادات عبر حالاته الثلاث (ليس في هذا الإنشاء، غير متصل، متصل):
function folderSettingsState() {
var fs = window.WebToApkFS;
if (!fs || !fs.isAvailable()) return 'unavailable';
return fs.hasAccess() ? 'connected' : 'not-connected';
}
var row = document.getElementById('folder-row');
switch (folderSettingsState()) {
case 'unavailable': row.hidden = true; break;
case 'not-connected': row.querySelector('button').textContent = 'Choose a folder'; break;
case 'connected': row.querySelector('button').textContent = 'Disconnect'; break;
}علّم الصفحة قبل رسمها، ليتمكن CSS من إظهار عناصر التحكم الخاصة بالمجلدات دون وميض:
// In a <script> inside <head>: the raw method is already there.
if (window.WebToApk && typeof window.WebToApk.isFolderAccessEnabled === 'function' &&
window.WebToApk.isFolderAccessEnabled() === 'true') {
document.documentElement.classList.add('has-folders'); // .has-folders .folder-only { display: block }
}ملاحظات: هذا لا يخبر هل اختير مجلد بعد - استخدم hasFolderAccess() لذلك. في المتصفح لا يوجد window.WebToApk، لذا يجيب الفحص أعلاه بـ false دون أن يرمي خطأ.
listFolderEntries الجسر (bridge)#
window.WebToApk.listFolderEntries(relativePath: String): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
يسرد الملفات والمجلدات الفرعية داخل مجلد من المجلد الممنوح.
القيمة المُرجَعة: **نص** JSON، بشكل متزامن: مصفوفة من { name, uri, isDirectory, length, lastModified } (length بالبايت، lastModified بالمللي ثانية منذ 1970). وتحلله لك WebToApkFS.list(path). المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول) ومجلد ممنوح.
باستخدام المساعد ('' هو المجلد الممنوح نفسه):
function showFolder(path) {
if (!window.WebToApkFS) return;
var entries = window.WebToApkFS.list(path); // already an array
entries.forEach(function (e) {
console.log(e.isDirectory ? '[dir] ' : ' ', e.name, e.length + ' bytes');
});
}
showFolder(''); // the root
showFolder('notes'); // a sub-folderالاستدعاء الخام، الذي يُرجع نصًا عليك تحليله:
if (window.WebToApk && typeof window.WebToApk.listFolderEntries === 'function') {
var entries = JSON.parse(window.WebToApk.listFolderEntries('notes'));
console.log(entries.length + ' entries');
}اعثر على أحدث ملف نسخة احتياطية واعرض متى أُنشئ:
function latestBackup() {
if (!window.WebToApkFS) return null;
var backups = window.WebToApkFS.list('backups').filter(function (e) {
return !e.isDirectory && /\.json$/i.test(e.name);
});
backups.sort(function (a, b) { return b.lastModified - a.lastModified; }); // newest first
return backups[0] || null;
}
var last = latestBackup();
document.getElementById('last-backup').textContent =
last ? 'Last backup: ' + new Date(last.lastModified).toLocaleString() : 'No backups yet';ملاحظات: مجلد فارغ، مجلد غير موجود، مسار هو ملف، مسار فيه ..، لا مجلد ممنوح، وصفحة من موقع غريب - كلها تجيب بـ [] - استدعِ hasFolderAccess() أولاً إذا احتجت إلى التمييز بـ "لا مجلد". المسارات نسبية وتستخدم / ولا تصعد فوق المجلد الممنوح.
mkdir الجسر (bridge)#
window.WebToApk.mkdir(relativePath: String): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
ينشئ مجلدًا داخل المجلد الممنوح، بما في ذلك أي مجلدات أم ناقصة.
القيمة المُرجَعة: النص "true" أو "false"، بشكل متزامن. وتُرجع WebToApkFS.mkdir(path) قيمة منطقية حقيقية. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول) ومجلد ممنوح.
باستخدام المساعد:
if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
var ok = window.WebToApkFS.mkdir('photos/2026/june'); // creates photos, 2026 and june
if (!ok) console.log('Could not create the folder');
}الاستدعاء الخام:
if (window.WebToApk && typeof window.WebToApk.mkdir === 'function') {
var made = window.WebToApk.mkdir('backups') === 'true';
}أنشئ مجلدًا مؤرخًا لتصدير واحد واكتب فيه كل عنصر:
function exportAll(items) {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
var dir = 'exports/' + new Date().toISOString().slice(0, 10); // e.g. exports/2026-09-25
if (!window.WebToApkFS.mkdir(dir)) {
alert('Could not create ' + dir + ' (is a file with that name in the way?)');
return false;
}
items.forEach(function (item) {
window.WebToApkFS.writeText(dir + '/' + item.id + '.json', JSON.stringify(item));
});
return true;
}ملاحظات: يجيب بـ "true" عندما يكون المجلد موجودًا بالفعل، لذا يمكن استدعاؤه بأمان قبل كل حفظ. ويجيب بـ "false" عندما **يعترض** ملف بهذا الاسم، أو يحتوي المسار على ..، أو لا يوجد مجلد ممنوح.
readTextFile الجسر (bridge)#
window.WebToApk.readTextFile(relativePath: String): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
يقرأ ملفًا نصيًا من المجلد الممنوح بصيغة UTF-8.
القيمة المُرجَعة: نص، بشكل متزامن: نص الملف، أو "". وتُرجع WebToApkFS.readText(path) الشيء نفسه. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول) ومجلد ممنوح.
"" تعني "ملف فارغ" أو "لا يوجد ملف كهذا"، لذا افحص بـ WebToApkFS.stat() عندما يهم الفرق:
function loadNote(path) {
if (!window.WebToApkFS) return null;
var info = window.WebToApkFS.stat(path); // { ok: true, name, size, ... } or { ok: false, error: 'not-found' }
if (!info.ok) return null; // the file does not exist
return window.WebToApkFS.readText(path); // '' here really is an empty file
}
var text = loadNote('notes/today.txt');
document.getElementById('editor').value = text === null ? '' : text;الاستدعاء الخام:
if (window.WebToApk && typeof window.WebToApk.readTextFile === 'function') {
var raw = window.WebToApk.readTextFile('notes/today.txt');
console.log(raw.length + ' characters');
}استورد كل ملاحظات .txt و.md من مجلد:
function importNotes(folder) {
if (!window.WebToApkFS) return [];
return window.WebToApkFS.list(folder)
.filter(function (e) { return !e.isDirectory && /\.(txt|md)$/i.test(e.name); })
.map(function (e) {
return {
title: e.name.replace(/\.(txt|md)$/i, ''),
body: window.WebToApkFS.readText(folder + '/' + e.name)
};
});
}
renderNotes(importNotes('notes'));ملاحظات: يُقرأ الملف كله في الذاكرة - مناسب للملاحظات وJSON، وليس للملفات الكبيرة؛ استخدم WebToApkFS.readBytes لتلك. البايتات غير الصالحة كـ UTF-8 تعود برمز الاستبدال. مسار مجلد، أو مسار فيه ..، أو عدم وجود مجلد ممنوح، أو صفحة من موقع غريب - كلها تجيب "".
renameEntry الجسر (bridge)#
window.WebToApk.renameEntry(relativePath: String, newName: String): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
يعيد تسمية ملف أو مجلد داخل المجلد الممنوح.
القيمة المُرجَعة: النص "true" أو "false"، بشكل متزامن. وتُرجع WebToApkFS.rename(path, newName) قيمة منطقية حقيقية. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول) ومجلد ممنوح.
newName اسم عادي وليس مسارًا - يبقى المدخل في المجلد نفسه:
if (window.WebToApkFS) {
var ok = window.WebToApkFS.rename('notes/today.txt', 'journal.txt'); // -> notes/journal.txt
if (!ok) alert('Rename failed. Is there already a file called journal.txt?');
}الاستدعاء الخام:
if (window.WebToApk && typeof window.WebToApk.renameEntry === 'function') {
var renamed = window.WebToApk.renameEntry('old-folder', 'new-folder') === 'true';
}احتفظ بالنسخة الاحتياطية السابقة بإعادة تسميتها قبل كتابة الجديدة:
function rotateBackup(json) {
var fs = window.WebToApkFS;
if (!fs || !fs.hasAccess()) return false;
if (fs.stat('backup.json').ok) {
var stamp = new Date().toISOString().replace(/[:.]/g, '-'); // no characters a file name cannot hold
fs.rename('backup.json', 'backup-' + stamp + '.json'); // stays in the same folder
}
return fs.writeText('backup.json', json);
}ملاحظات: يفشل عندما يكون لدى مدخل آخر newName بالفعل، أو عندما تكون newName فارغة أو تحتوي على / أو \، أو عندما لا يوجد المسار، أو عندما لا يوجد مجلد ممنوح. لنقل ملف إلى مجلد آخر، اكتبه هناك واحذف القديم.
requestFolderAccess الجسر (bridge)#
window.WebToApk.requestFolderAccess(requestId: String)
يفتح منتقي مجلدات النظام ويحفظ صلاحية القراءة/الكتابة للمجلد المختار. تصل الإجابة على `WebToApkOnFolderAccessResult`؛ ولا يمكن فتح أكثر من منتقٍ واحد في وقت واحد، لذا يُجاب الطلب المقدَّم أثناء فتح منتقٍ فورًا بـ `error:"busy"` ويحتفظ المنتقي المفتوح بإجابته.
مثال
يفتح منتقي مجلدات النظام ليتمكن المستخدم من منح التطبيق مجلدًا واحدًا. تستخدم الصفحات عادةً WebToApkFS.requestAccess() التي تغلّف هذا الاستدعاء في Promise.
القيمة المُرجَعة: لا شيء. تعود الإجابة لاحقًا عبر استدعاء داخلي يملكه WebToApkFS؛ وتتحقق WebToApkFS.requestAccess() بـ { success, folderUri, requestId }، مع error عندما تكون success خاطئة. المتطلبات: حدّد Native Folder Access (SAF) في الخطوة 4 (الوصول) عند الإنشاء.
اطلب من نقرة زر، باستخدام المساعد:
document.getElementById('choose-folder').addEventListener('click', async function () {
if (!window.WebToApkFS) {
alert('Folder access is not available in this app.');
return;
}
var r = await window.WebToApkFS.requestAccess();
if (r.success) {
console.log('Folder granted:', r.folderUri); // content://.../tree/...
} else {
console.log('No folder chosen'); // user cancelled, or Android refused the grant
}
});اطلب في المرة الأولى فقط، ثم جهّز المجلد الفرعي الخاص بالتطبيق:
async function ensureFolder() {
if (!window.WebToApkFS || !window.WebToApkFS.isAvailable()) return false;
if (window.WebToApkFS.hasAccess()) return true; // granted on an earlier run
var r = await window.WebToApkFS.requestAccess();
if (!r.success) return false;
window.WebToApkFS.mkdir('exports'); // true even if it already exists
return true;
}
document.getElementById('export').addEventListener('click', async function () {
if (!(await ensureFolder())) return;
window.WebToApkFS.writeText('exports/list.csv', buildCsv());
});أخبر المستخدم بدقة لماذا لم يُوصل أي مجلد، باستخدام رموز error الحقيقية:
var folderMessages = {
cancelled: 'No folder was chosen.',
not_persisted: 'Android would not keep access to that folder. Please choose another one.',
busy: 'The folder picker is already open.',
disabled: 'This app was built without folder access.',
foreign_origin: 'A folder can only be connected from the app\'s own pages.',
no_picker: 'This phone has no folder picker.'
};
document.getElementById('choose-folder').addEventListener('click', async function () {
if (!window.WebToApkFS) return;
var r = await window.WebToApkFS.requestAccess();
document.getElementById('status').textContent = r.success
? 'Folder connected.'
: (folderMessages[r.error] || 'Could not connect a folder.');
});ملاحظات: لا تستدعِ الدالة الخام بنفسك: إجابتها الوحيدة هي الاستدعاء الذي يستمع إليه المساعد. تكون success بقيمة false عندما يلغي المستخدم (error:'cancelled')، وعندما لا يحتفظ Android بالمنح ('not_persisted')، وفورًا عندما لم يفعّل الإنشاء المفتاح ('disabled') أو الصفحة المعروضة ليست تابعة للتطبيق ('foreign_origin'). يُحتفظ بالمنح عبر إعادة تشغيل التطبيق؛ والاختيار مرة أخرى يستبدل المجلد. لا يمكن فتح أكثر من منتقٍ واحد في وقت واحد - فالطلب المقدَّم أثناء فتح منتقٍ يُجاب فورًا بـ error:'busy'، ويحتفظ المنتقي المفتوح بإجابته.
writeTextFile الجسر (bridge)#
window.WebToApk.writeTextFile(relativePath: String, content: String): String
وصول آمن لـ Play إلى المجلدات باستخدام ACTION_OPEN_DOCUMENT_TREE مع أذونات URI دائمة. الاستخدام في JavaScript: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." أو "" window.WebToApk.clearFolderAccess() // يحرر الوصول المحفوظ استدعاء من Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) تقول `error` (الوسيط الرابع، "" عند النجاح) سبب فشل الطلب: "disabled" و"foreign_origin" و"busy" (منتقٍ مفتوح بالفعل لطلب سابق - ويحصل ذلك الطلب على إجابته) و"cancelled" و"not_persisted" و"no_picker".
موصوفة ضمن مجموعتها، Native Folder Access (SAF) Bridge، وليس بمفردها.
مثال
يكتب ملفًا نصيًا (UTF-8) في المجلد الممنوح، فيُنشئه أو يستبدل محتواه.
القيمة المُرجَعة: النص "true" أو "false"، بشكل متزامن. وتُرجع WebToApkFS.writeText(path, text) قيمة منطقية حقيقية. المتطلبات: تحديد Native Folder Access (SAF) في الخطوة 4 (الوصول) ومجلد ممنوح.
باستخدام المساعد:
function saveNote(path, text) {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) {
alert('Choose a folder first.');
return false;
}
var ok = window.WebToApkFS.writeText(path, text); // missing parent folders are created
if (!ok) alert('Could not save ' + path);
return ok;
}
saveNote('notes/2026/today.txt', document.getElementById('editor').value);الاستدعاء الخام، وإجابته نص:
if (window.WebToApk && typeof window.WebToApk.writeTextFile === 'function') {
var saved = window.WebToApk.writeTextFile('data/settings.json', JSON.stringify({ theme: 'dark' })) === 'true';
}أضف سطرًا إلى سجل يومي - الكتابة تستبدل الملف، لذا اقرأه أولاً:
function appendLog(line) {
if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return false;
var now = new Date().toISOString();
var path = 'logs/' + now.slice(0, 10) + '.log'; // one file per day
var old = window.WebToApkFS.readText(path); // '' the first time
return window.WebToApkFS.writeText(path, old + now + ' ' + line + '\n');
}
appendLog('Order #1042 marked as shipped');ملاحظات: يُستبدل الملف الموجود بالكامل. يفشل عندما يكون المسار مجلدًا موجودًا، أو يحتوي على ..، أو لا يوجد مجلد ممنوح. يُؤخذ نوع الملف من الامتداد (.json و.txt و.md…)، فالاسم الذي تعطيه هو الاسم على القرص.