ابدأ من هنا
كتابة HTML لتطبيقك
ما يعرضه تشغيل التطبيق لصفحتك - جهات الاتصال والرسائل القصيرة والإشعارات ومعلومات الجهاز والقياسات الحيوية - وقواعد استدعائه بشكل صحيح
1القاعدة 1 - تحقق دائمًا أولًا#
window.WebToApk موجود فقط داخل تطبيقك المبني. وفي المتصفح يكون غير موجود.
لذا تحقق دائمًا قبل الاستدعاء. وإلا تتعطل صفحتك في كل مكان عدا داخل التطبيق.
بعض الأشياء لا تحتاج أي تحقق: navigator.vibrate() وspeechSynthesis وNotification تعمل بالفعل - يربطها التطبيق لك. استخدم كود الويب العادي فقط.
// GOOD — safe everywhere
if (window.WebToApk) {
WebToApk.playClick();
}
// BAD — this breaks your page in a browser
WebToApk.playClick();2القاعدة 2 - بعض الإجابات تأتي لاحقًا#
قراءة جهات الاتصال أو الرسائل أو سجل المكالمات تستغرق وقتًا. لذا لا تجيب هذه فورًا. ولو فعلت، لتجمد تطبيقك.
تعطي الاستدعاء اسمًا (id). تصل الإجابة لاحقًا كحدث. وأنت تستمع لذلك الحدث.
أسماء الأحداث: appmint:contacts, appmint:calllog, appmint:sms, appmint:sms-received, appmint:notification-action.
// 1. Listen for the answer
window.addEventListener('appmint:contacts', function (e) {
if (e.detail.error) {
alert('Problem: ' + e.detail.error);
return;
}
console.log(e.detail.contacts);
});
// 2. Ask the question ('my1' is any name you choose)
WebToApk.listContacts('my1', 50, 0);3القاعدة 3 - يجب أن يكون مفتاحان قيد التشغيل#
تحتاج ميزة الهاتف إلى شيئين:
- فعّلته في الخطوة 4 (Access) عند بناء التطبيق. وإلا تحصل على الخطأ 'not_enabled' ولا يرى المستخدم شيئًا.
- قال المستخدم نعم على الهاتف. وإذا قال لا، تحصل على الخطأ 'permission_denied'.
عالج الخطأين دائمًا ليعرف المستخدم ما حدث.
طريقة أسهل: pickContact() وcomposeSms() لا تحتاجان إلى أي إذن إطلاقًا. استخدمهما عندما تناسبان - تؤديان العمل نفسه بمتاعب أقل.
window.addEventListener('appmint:contacts', function (e) {
if (e.detail.error === 'not_enabled') {
alert('Turn on Contacts when you build the app');
} else if (e.detail.error === 'permission_denied') {
alert('Please allow contacts access');
} else {
showContacts(e.detail.contacts);
}
});4إشعارات بالصور والأزرار#
يأخذ notify() قائمة خيارات. يمكنك إضافة صورة كبيرة وأيقونة وأزرار وشريط تقدم.
يمكن أن تكون channel: urgent أو default أو quiet أو ongoing.
استخدم ongoing:true لإشعار لا يستطيع المستخدم إزالته بالسحب. ملاحظة: ما زال بإمكانه إزالته من إعدادات Android. لا يمكن لأي تطبيق جعله مستحيل الإزالة.
استخدم tag لتسميته. الإرسال مجددًا بالوسم نفسه يستبدل القديم بدلًا من إضافة جديد.
WebToApk.notify(JSON.stringify({
title: 'Order shipped',
body: 'Arriving Tuesday',
channel: 'urgent',
image: 'https://mysite.com/box.jpg',
actions: [{ id: 'track', label: 'Track' }],
tag: 'order-482'
}));
// Know which button the user pressed
window.addEventListener('appmint:notification-action', function (e) {
if (e.detail.actionId === 'track') showTracking();
});5معلومات الهاتف والبصمة#
لا تحتاج getDeviceInfo() إلى أي إذن. تعطيك إصدار Android ومعرّف الإصدار (buildId) وطراز الهاتف وحجم الشاشة ومستوى البطارية وغير ذلك.
عن معرّف الجهاز: لا يوجد IMEI ولا رقم تسلسلي. حجبه Android عن كل التطبيقات منذ Android 10. استخدم getInstallId() بدلًا منه - معرّف ثابت لهذا التثبيت يبقى كما هو بعد التحديثات.
var info = JSON.parse(WebToApk.getDeviceInfo());
info.android.release; // "14"
info.android.buildId; // "TQ3A.230805.001" <- version ID
info.hardware.model; // "Pixel 7"
info.runtime.batteryLevel; // 82
// Fingerprint
window.__webToApkAuth = window.__webToApkAuth || {};
window.__webToApkAuth['unlock'] = function (json) {
var r = JSON.parse(json);
if (r.ok) showMyApp();
};
WebToApk.authenticateBiometricEx('unlock', JSON.stringify({
title: 'Unlock',
allowDeviceCredential: true
}));6اجعل صفحتك تملأ الشاشة#
عند تفعيل Fullscreen، يخفي التطبيق أشرطة Android نيابةً عنك. لكن أضف CSS هذا حتى لا يختفي محتواك تحت نتوء الهاتف.
مهم: يخفي Fullscreen أشرطة ANDROID (الساعة والبطارية وأزرار الرجوع). أما الشريط الملون الخاص بتطبيقك في الأعلى فهو مفتاح مختلف اسمه 'Show top bar'. إذا ما زلت ترى شريطًا بعد تفعيل Fullscreen، فهذا هو الذي يجب إيقافه.
أيضًا: اختبر تطبيقك مدوّرًا. ينتقل النتوء إلى الجانب في الوضع الأفقي.
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
min-height: 100vh; /* old phones */
min-height: 100dvh; /* correct when bars are hidden */
}7التنزيلات تحتفظ باسم الملف الذي اخترته#
كود التنزيل العادي لديك يعمل بالفعل. عندما تحفظ صفحتك ملفًا - تقرير PDF أو تصدير CSV أو نسخة احتياطية - يلتقطه التطبيق ويفتح ورقة 'Save as…' في Android واسم ملفك مكتوب مسبقًا.
يغطي هذا كل الطرق المعتادة: <a download="..."> وa.click() من الكود وhtml2pdf / jsPDF وFileSaver.js saveAs() وwindow.open() على رابط blob. وكائن File يحمل اسمه معه.
إذا فضّلت الطلب مباشرة بدل بناء وسم <a>، استخدم AppMint.downloadFile().
شيء يجب معرفته: الاسم موجود فقط في JavaScript الخاص بصفحتك - لا يراه Android بنفسه. لذا اضبط download= (أو مرّر اسمًا) في كل مرة، وإلا حُفظ الملف باسم 'download'.
// The usual way — saves as Site_Report_2026-08.pdf
const blob = await html2pdf().from(el).outputPdf('blob');
const a = document.createElement('a');
a.download = 'Site_Report_2026-08.pdf';
a.href = URL.createObjectURL(blob);
a.click();
// Or ask directly
AppMint.downloadFile(base64String, 'Ledger_Q3.csv', 'text/csv');
AppMint.saveBlob(myBlob, 'Backup.json');8اجعل زر الرجوع يفعل ما يتوقعه تطبيقك#
إذا كان تطبيقك يغيّر الشاشات بإظهار العناصر وإخفائها (دون تغيير الرابط ودون history.pushState)، فلا يرى زر الرجوع في Android تلك الشاشات - سجله فارغ، فيخرج الرجوع من التطبيق من أي مكان.
سجّل معالج رجوع وقرر بنفسك: أعد true عندما عالجت الضغطة (أغلقت قائمة، رجعت شاشة)، وأعد false لتعمل السلوكية العادية - الرجوع في سجل الصفحات، ثم تأكيد الخروج، ثم الخروج.
التطبيقات التي تستخدم history.pushState لكل شاشة لا تحتاج هذا: الرجوع يمشي بالفعل في سجلها. ولا تقلق من تجميد التطبيق - إذا علق معالجك يومًا ما، يستمر زر الرجوع في الهاتف بالعمل أصليًا بعد لحظة قصيرة.
AppMint.setBackHandler(() => {
if (isMenuOpen) { closeMenu(); return true; } // consumed
if (screen !== 'home') { goTo('home'); return true; }
return false; // nothing open — normal exit behaviour
});
// Or listen instead: e.preventDefault() consumes the press
window.addEventListener('appmint:back', e => {
if (closeTopmost()) e.preventDefault();
});9استقبل ما يشاركه الناس إلى تطبيقك - روابط وصور وفيديوهات وملفات#
فعّل 'Receive shares from other apps' في معالج البناء وحدد ما يقبله تطبيقك: نصوص وروابط، وملفات بيانات (JSON وCSV وTXT وXML وMarkdown وGPX وTCX وFIT)، وصور، وفيديوهات، وصوت، ومستندات (PDF وWord وExcel وPowerPoint). يظهر تطبيقك بعدها في قائمة المشاركة لكل تطبيق آخر لهذه الأنواع بالضبط - تطبيق ملاحظات للنص، ومشغّل للفيديو.
يُقرأ الرابط أو النص (مشاركة فيديو YouTube من تطبيق YouTube ترسل رابطه) بـ WebToApk.getSharedText() ويُعلَن كـ appmint:shared. ويُطلب الملف بـ AppMint.getOpenedFile() - اطلبه متى كنت جاهزًا، والملف ينتظرك. هذا مهم لتطبيقات React وVue وAngular: فهي تنهي بدءها بعد تحميل الصفحة، لذا كان التطبيق الذي يستمع للحدث فقط يفوّت الملف ويعرض شاشته الرئيسية فقط.
يخبرك f.kind بما وصل: image أو video أو audio أو document أو data. تأتي الصور والفيديوهات والصوت كـ f.url - عنوان قابل للبث والتقديم تضعه مباشرة في <video src> أو <img src> أو <audio src>، فلا يحتاج مقطع 300 MB إلى أن يتسع في الذاكرة؛ ويجلب f.getFile() كائن File عندما تحتاج البايتات فعلًا. وتأتي ملفات البيانات والمستندات أيضًا مع f.file، بالإضافة إلى f.text للصيغ النصية وf.base64 للملفات الثنائية الصغيرة كاختصارات. والصورة المشاركة مع تعليق تسلّم التعليق كنص مشارَك أيضًا.
// A link or text someone shared
const shared = JSON.parse(WebToApk.getSharedText() || 'null');
if (shared) saveLink(shared.url || shared.text);
window.addEventListener('appmint:shared', e => saveLink(e.detail.url || e.detail.text));
// A photo, video, audio clip, document or data file — ask on startup, however late
const f = await AppMint.getOpenedFile(); // null if opened normally
if (f) {
if (f.kind === 'video') player.src = f.url; // streams, seekable
else if (f.kind === 'image') img.src = f.url;
else if (f.kind === 'document') await upload(await f.getFile()); // a real File
else importActivity(f.text ?? await f.file.text()); // data files
}
// Or listen, if you prefer
window.addEventListener('appmint:fileopen', e => handle(e.detail));10مستشعرات Bluetooth - Web Bluetooth القياسي#
أحزمة معدل ضربات القلب ومستشعرات الإيقاع والسرعة وقياس القدرة والمدربات الذكية والموازين - أي شيء يتحدث Bluetooth LE.
استخدم navigator.bluetooth، وهو الكود نفسه الذي تكتبه لـ Chrome على سطح المكتب. يوفره AppMint داخل التطبيق؛ أما WebView العادي في Android فلا يملكه، ولهذا لا يفعل هذا الكود شيئًا في منشئات التطبيقات الأخرى.
تُرسم قائمة اختيار الجهاز بواسطة التطبيق، مثل المتصفح - ولا تستطيع صفحتك الوصول إلا إلى الجهاز الذي اختاره المستخدم.
فعّل إذن Bluetooth في الخطوة 4 (Access). إذا كان كودك يذكر navigator.bluetooth، يفعّله AppMint لك عند اختيار ZIP.
غير متاح: getDevices() وwatchAdvertisements() وrequestLEScan() - ترفض مع NotSupportedError، فتحقق قبل استخدامها.
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService('heart_rate');
const chr = await service.getCharacteristic('heart_rate_measurement');
chr.addEventListener('characteristicvaluechanged', e => {
const v = e.target.value; // a DataView
const bpm = (v.getUint8(0) & 1) ? v.getUint16(1, true) : v.getUint8(1);
document.getElementById('bpm').textContent = bpm;
});
await chr.startNotifications();
device.addEventListener('gattserverdisconnected', () => showReconnect());11رفع الصور - منتقي صور Android، بدون إذن#
استخدم حقل ملف عاديًا. لا شيء آخر لإضافته.
في التطبيق، يفتح <input type="file" accept="image/*"> منتقي صور Android: صور المستخدم في شبكة، وتحصل صفحتك فقط على ما يختاره. بلا إذن تخزين وبلا نافذة 'Allow access to photos?'. ويعمل على الهواتف القديمة أيضًا - إذ تثبّت خدمات Google Play المنتقي هناك.
- accept="image/*" - صور فقط
- accept="video/*" - فيديوهات فقط
- accept="image/*,video/*" - كلاهما
- أضف multiple - يمكن للمستخدم اختيار عدة ملفات
- أضف capture="environment" - يفتح الكاميرا بدلًا منه (يتطلب تفعيل Camera في الخطوة 4 (Access))
أضف ملفات أخرى (accept="image/*,.pdf") فيفتح منتقي الملفات العادي بدلًا منه، ليمكن اختيار PDF أيضًا.
<input type="file" id="photos" accept="image/*" multiple>
<script>
document.getElementById('photos').onchange = function (e) {
for (const file of e.target.files) {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
document.body.appendChild(img);
}
};
</script>12امسح رمز QR أو باركود - بسطر واحد#
AppMint.scanCode() يفتح شاشة الماسح من Google. يوجّه المستخدم الكاميرا، ويصلك الرمز.
بدون إذن كاميرا. بدون عنصر video. لا شيء لترسمه. يقرّب الماسح تلقائيًا للرموز الصغيرة. في أول مرة على الهاتف، تنزّل خدمات Google Play الماسح (بضع ثوانٍ).
تحتوي الإجابة على value (النص في الرمز) وformat (qr_code وean_13 …). ولرموز QR الشائعة تحصل أيضًا على الأجزاء جاهزة: url وwifi (ssid وpassword) وphone وemail وsms وgeo وcontact.
إذا كانت ok تساوي false، يخبرك code بالسبب: cancelled (أغلقه المستخدم) أو unavailable (لا توجد خدمات Google Play أو فشل التنزيل) أو busy (مسح مفتوح بالفعل).
AppMint.scanCode موجود داخل التطبيق فقط - تحقق منه أولًا.
async function scan() {
if (!window.AppMint || !AppMint.scanCode) return; // in a browser
const r = await AppMint.scanCode({ formats: ['qr_code'] });
if (!r.ok) return; // closed, or no scanner
console.log(r.value); // the text inside the code
if (r.url) location.href = r.url.url;
}
// Any code type: AppMint.scanCode()
// Let the user type it when it will not scan: AppMint.scanCode({ manualInput: true })13مفاتيح المرور - سجّل الدخول ببصمة الإصبع أو الوجه، بدون كلمة مرور#
إذا كان موقعك يستخدم مفاتيح المرور بالفعل، فهي تعمل الآن داخل تطبيقك أيضًا. لا يستطيع WebView العادي في Android التعامل مع مفاتيح المرور؛ أما AppMint فيربط navigator.credentials بمدير مفاتيح المرور في Android، فتعمل مكتبات مثل SimpleWebAuthn وHanko وCorbado وAuth0 دون تغيير.
تحتاج إلى خادم ينشئ خيارات مفتاح المرور ويتحقق من الإجابة (مثل SimpleWebAuthn في الواجهة الخلفية). لا يمكن التحقق من مفتاح المرور داخل الصفحة.
أمران للإعداد مرة واحدة:
- يجب أن يقول موقعك إنه يثق بالتطبيق. ضع الملف من
AppMint.passkey.assetLinks() على https://YOUR-DOMAIN/.well-known/assetlinks.json. خذه من التطبيق المثبّت من Google Play - توقّع Play تطبيقك بمفتاحها الخاص، لذا لن يطابق الملف المأخوذ من APK اختبار مستخدمي Play. يجب أن يكون النطاق هو rpID لخادمك تمامًا (www وبدون www مختلفان).
- يجب أن يقبل خادمك أصل (origin) التطبيق. تقول الإجابة من التطبيق origin 'android:apk-key-hash:…' بدل https://… - احصل على تلك القيمة من
AppMint.passkey.origin() وأضفها إلى الأصول المسموحة في خادمك.
الأخطاء: NotAllowedError = ألغى المستخدم أو لا يملك مفتاح مرور على هذا الهاتف. InvalidStateError = مفتاح المرور هذا موجود بالفعل. SecurityError = لا يدرج assetlinks.json التطبيق.
تحتاج مفاتيح المرور إلى Android 9 أو أحدث مع خدمات Google Play - يخبرك AppMint.passkey.isAvailable(). الملء التلقائي لمفتاح المرور في حقل نصي (mediation: 'conditional') غير متاح في التطبيقات؛ ابدأ تسجيل الدخول من زر.
// Your normal passkey code works as it is:
const cred = await navigator.credentials.get({ publicKey: options });
// Or with your server's JSON options (SimpleWebAuthn style):
const options = await (await fetch('/passkey/login-options')).json();
const answer = await AppMint.passkey.get(options);
await fetch('/passkey/login-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(answer)
});
// Once, on the Play-installed app — copy these to your server setup:
console.log(JSON.stringify(AppMint.passkey.assetLinks(), null, 2));
console.log(AppMint.passkey.origin());14ودجت الشاشة الرئيسية - اعرض بياناتك على الشاشة الرئيسية#
لتطبيقك ودجت للشاشة الرئيسية. يضيفه المستخدم من قائمة ودجتات الهاتف (اضغط مطولًا على الشاشة الرئيسية ثم Widgets)، أو تفتح ورقة الإضافة بـ AppMint.widget.requestPin().
يبقى الودجت فارغًا ("Nothing to show yet") حتى تملأه صفحتك. إنه بطاقة Android صغيرة وليس صفحة ويب، فلا يمكنه عرض HTML الخاص بك. تعطيه عنوانًا وقيمة كبيرة واحدة وحتى ستة أسطر قصيرة وألوانًا.
استدعِ AppMint.widget.set() عند فتح التطبيق، ومجددًا في كل مرة تتغير البيانات التي يعرضها. يحتفظ الودجت بآخر بطاقة حتى بعد إعادة التشغيل. route هو المكان الذي يفتح فيه تطبيقك عند الضغط على الودجت.
function updateWidget() {
if (!window.AppMint || !AppMint.widget) return; // in a browser
AppMint.widget.set({
title: 'Today',
value: '3 events',
lines: ['09:00 Dentist', '13:00 Lunch with Sam', '18:30 Gym'],
route: '/',
bg: '#0E7C86', ink: '#FFFFFF', accent: '#FFE08A'
});
}
updateWidget(); // on start-up, and after every change
// An "Add to home screen" button:
async function addWidget() {
if ((await AppMint.widget.placedCount()) > 0) return;
await AppMint.widget.requestPin();
}15حماية النسخ وقائمة الضغط المطوّل#
مفتاحان في الخطوة 3 (Integrate)، تحت Display Options. كلاهما مغلق حتى تفعّله.
- قائمة الضغط المطوّل على الروابط والصور: اضغط مطولًا على رابط أو صورة لفتحها أو نسخها أو مشاركتها أو حفظها. مغلقة افتراضيًا، فالضغط المطوّل على رابط لا يفعل شيئًا. ما زال بإمكان الصفحة رفض القائمة على عنصر واحد بإلغاء حدث contextmenu.
- حماية النسخ: لا يستطيع أحد تحديد النص في تطبيقك أو نسخه أو قصّه. الكتابة في النماذج تعمل كالمعتاد. كما أنها تبقي قائمة الضغط المطوّل مغلقة.
تستطيع صفحتك أن تعرف: AppMint.copyProtection تكون true عند تفعيل الحماية، وغير موجودة عند إيقافها (وفي المتصفح). وهي للقراءة فقط. أخفِ أزرار النسخ الخاصة بك عند تفعيلها، لأنها لن تنسخ شيئًا.
const protectedApp = !!(window.AppMint && window.AppMint.copyProtection === true);
// A Copy button would copy nothing, so hide it
document.querySelectorAll('[data-copy-button]').forEach(btn => {
btn.hidden = protectedApp;
});16{exampleCount} مثالًا جاهزًا - افتحها هنا#
كود يعمل يمكنك نسخه: إرسال SMS وقراءة جهات الاتصال وعرض إشعار بصورة وقفل بالبصمة ومعلومات الهاتف وحفظ الملفات ومستشعرات Bluetooth وغيرها. كل مثال قصير ومشروح بكلمات بسيطة.
يفتح داخل AppMint. يمكنك البحث فيه، ولكل كتلة كود زر نسخ.
في الشاشة نفسها أيضًا 'All methods' - كل الأشياء البالغ عددها {methodCount} التي يستطيع تطبيقك استدعاءها، ويأتي كل منها بمثال يعمل يمكنك نسخه. تُبنى هذه القائمة من تشغيل التطبيق نفسه، فتطابق دائمًا ما يستطيع تطبيقك فعله بالضبط.
17احفظ الأمثلة على هاتفك أو حاسوبك#
يحفظ ثلاثة ملفات في Downloads/AppMint/Guides:
- الأمثلة - الأمثلة البالغ عددها {exampleCount} أعلاه
- قائمة الدوال - كل الدوال البالغ عددها {methodCount}
- index.html - صفحة اختبار جاهزة
تحتوي صفحة الاختبار على أزرار لمعلومات الجهاز والاهتزاز والإشعارات ومنتقي جهات الاتصال. ابنها كتطبيق لترى كل شيء يعمل، ثم حوّلها إلى تطبيقك. وتفتح أيضًا في متصفح عادي دون مشكلة - لا ينكسر شيء، تقول فقط إنها ليست داخل التطبيق.
في التطبيق، يحفظ زر هذه الخطوة هذه الملفات على هاتفك. أما هنا فهي تنزيلات مباشرة.
AppMint-examples.md AppMint-all-methods.md index.html18منبّه يرن فوق شاشة القفل#
فعّل Alarm clock في خطوة Access. يرن scheduleNotificationEx مع alarm: true بصوتك حتى يوقفه المستخدم أو يؤجّله، حتى والتطبيق مغلق. يمكن أن يتكرر يوميًا أو أسبوعيًا أو في أيام العمل أو شهريًا أو سنويًا، أو اختر أيام الأسبوع.
قبل الجدولة، اسأل getAlarmSupport(). تجيب 'ready' أو 'not_enabled' أو 'needs_exact_alarm' أو 'needs_notification_permission'. ثم تتحكم stopAlarm(id) وsnoozeAlarm(id, minutes) وisAlarmRinging(id) بمنبّه يرن.
const B = window.WebToApk;
if (B && B.getAlarmSupport && B.getAlarmSupport() === 'ready') {
B.scheduleNotificationEx('wake', JSON.stringify({
at: firstAtMs, alarm: true, title: 'Wake up', repeat: 'weekdays', sound: '/audio/alarm.mp3'
}));
}
document.getElementById('snooze').onclick = () => B.snoozeAlarm('wake', 10);19مستوى صوت النظام#
يقرأ getVolume(stream) مستوى صوت الوسائط أو المنبّه أو الرنين أو الإشعارات أو المكالمة بصيغة JSON. يغيّره setVolume(stream, percent, showUi). يرسل watchVolume(true) حدث appmint:volume عند كل تغيير. يحتاج كتم صوت الرنين إلى صلاحية Do Not Disturb: requestDndAccess().
if (window.WebToApk && WebToApk.getVolume) {
const v = JSON.parse(WebToApk.getVolume('alarm'));
if (v.percent === 0) alert('Alarm volume is off');
window.addEventListener('appmint:volume', (e) => console.log(e.detail.stream, e.detail.percent));
WebToApk.watchVolume(true);
}20خدمة الخلفية والموقع في الخلفية والأسوار الجغرافية والمهام#
يُبقي startBackgroundService صفحتك وصوتك ومؤقتاتك تعمل أثناء غياب المستخدم، مع إشعار. يطلب requestBackgroundLocation 'Allow all the time'. يطلق addGeofence حدث الدخول أو الخروج أو المكوث والتطبيق مغلق. يشغّل AppMint.background.register كود صفحتك كل 15 دقيقة أو أكثر. وتبدأ كلها من جديد بعد إعادة تشغيل الهاتف.
تحتاج هذه إلى مفتاحها في المعالج (خطوة Access)؛ وبدونه يعود الاستدعاء بـ { ok: false, reason: 'not_enabled' }.
const r = JSON.parse(WebToApk.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Keeping data fresh' })));
if (!r.ok) console.warn(r.reason);
if (AppMint.background.isBackgroundRun()) {
window.addEventListener('appmint:background', async (e) => {
await refreshData();
AppMint.background.done(e.detail.runId, { ok: true });
});
} else {
AppMint.background.register('refresh', { intervalMinutes: 60 });
}21تقارير الانهيار#
فعّل Crash reports في خطوة Integrate. تُسجَّل الأخطاء غير المعالجة تلقائيًا. يسجّل logError(message, stack) خطأً التقطته، ويوسم setCrashUserId(id) التقارير بمعرّف المستخدم الخاص بك (وليس البريد الإلكتروني أبدًا)، وتعيد getLastCrash() أحدث انهيار بصيغة JSON، أو نصًا فارغًا. اقرأ التقارير في Update the App، ثم تطبيقك، ثم Crash reports.
if (window.WebToApk && WebToApk.logError) {
try { risky(); } catch (err) { WebToApk.logError(err.message, err.stack || ''); }
WebToApk.setCrashUserId('u_42');
const last = WebToApk.getLastCrash(); // '' when none
}22سلامة الجهاز وPlay Integrity#
فعّل Device integrity checks في خطوة Integrate. تعيد getDeviceIntegrity() علامات الروت والمحاكي والمُصحّح بصيغة JSON؛ وهي تحذير وليست دليلًا. للحصول على حكم يمكنك الوثوق به، استدعِ requestIntegrityToken(nonce, '') وأرسل الرمز من حدث appmint:integrity إلى خادمك أنت، الذي يطلب الحكم من Google Play.
const d = JSON.parse(WebToApk.getDeviceIntegrity());
if (d.rooted || d.emulator) alert('This phone is modified');
window.addEventListener('appmint:integrity', (e) => sendToServer(e.detail.token));
WebToApk.requestIntegrityToken(serverNonceBase64, '');23أيقونات تطبيق بديلة وTV#
أضف حتى 4 أيقونات بديلة في خطوة Basic. يبدّل setAppIcon(name) أيقونة المشغّل ('default' يعيدها)، ويسمّي getAppIcon() الحالية ويعرض getAvailableIcons() القائمة. يخبرك isTv() بأن التطبيق على تلفاز، فتعرض تخطيطًا مناسبًا للريموت.
if (window.WebToApk && WebToApk.setAppIcon) WebToApk.setAppIcon('dark');
if (window.WebToApk && WebToApk.isTv && WebToApk.isTv()) {
document.documentElement.classList.add('tv');
}24تسجيل الدخول عبر Facebook والتحليلات وRevenueCat#
تسجيل الدخول عبر Facebook مدعوم. في خطوة Integrate أدخل Facebook App ID وClient Token؛ يفتح تسجيل الدخول في تبويب متصفح ويعود إلى التطبيق. استدعِ AppMint.facebook.login(['public_profile', 'email']) وأرسل الرمز إلى خادمك. يسجّل AppMint.analytics (Firebase) وAppMint.facebook.logEvent (Meta App Events) فقط بعد موافقة المستخدم. ويتولى AppMint.revenuecat الاشتراكات بمفتاح Android العام الخاص بك.
const r = await AppMint.facebook.login(['public_profile', 'email']);
if (r.ok) await fetch('/api/login/facebook', { method: 'POST', body: JSON.stringify({ token: r.accessToken }) });
AppMint.analytics.log('share', { method: 'whatsapp' });
const offerings = await AppMint.revenuecat.getOfferings();25نص من الصور وملفات PDF والودجتات#
AppMint.textRecognition.recognize(image, { script }) يقرأ النص باللاتينية والديفاناغاري والصينية واليابانية والكورية. AppMint.pdf.fromPage() وfromHtml(html, options) ينشئان ملف PDF حقيقيًا دون نافذة طباعة. AppMint.widget.set(card) يملأ ودجت الشاشة الرئيسية.
const r = await AppMint.textRecognition.recognize(file, { script: 'devanagari' });
const pdf = await AppMint.pdf.fromHtml('<h1>Invoice</h1>', { fileName: 'invoice', save: 'share' });
await AppMint.widget.set({ title: 'Today', value: '3 events', route: '/calendar' });26بيانات الصحة ومنارات Bluetooth#
AppMint.health يقرأ ويكتب الخطوات ومعدل ضربات القلب والنوم والوزن والتمارين عبر Health Connect؛ فعّل Health data والأنواع في خطوة Access. يعثر AppMint.beacons.startScan() على منارات iBeacon وEddystone ويرسل أحداث appmint:beacon.
const a = await AppMint.health.isAvailable();
if (a.available) {
await AppMint.health.requestPermissions({ read: ['steps'] });
const steps = await AppMint.health.read('steps', { start: new Date(Date.now() - 864e5), end: new Date(), aggregate: true });
}
window.addEventListener('appmint:beacon', (e) => console.log(e.detail));
await AppMint.beacons.startScan();27إضافات جاهزة: SQLite والإشعارات والإشعارات الفورية والماسح وجهات الاتصال#
في الصفحات التي يقدمها التطبيق بنفسه، يحتوي window.Capacitor.Plugins على CapacitorSQLite (قاعدة بيانات خاصة) وLocalNotifications وPushNotifications وCapacitorBarcodeScanner وContacts. تحقق أولًا من وجود الإضافة: تطبيق وضع Website لا يملك window.Capacitor إلا في صفحات موقعه، وليس في النطاقات الأخرى.
const P = window.Capacitor && window.Capacitor.Plugins;
if (P && P.CapacitorBarcodeScanner) {
const s = await P.CapacitorBarcodeScanner.scanBarcode({ hint: 17 });
console.log(s.ScanResult);
}
if (P && P.LocalNotifications) await P.LocalNotifications.requestPermissions();