تطوير م. معين العباسي نظام فواتير الكهرباء لشركة العباسي لتوليد الطاقة الكهربائية. تطبيق أندرويد أصلي مبني بـ Flutter، يعمل بلا إنترنت، وكل البيانات محفوظة داخل جهاز المستخدم فقط.
| الإصدار | 1.1.0 (versionCode 2) |
| معرّف الحزمة | com.abbasisoft.billing |
| المنصة | Android (وواجهة ويب للمعاينة) |
| Flutter / Dart | 3.35.4 / 3.9.2 (مثبّتة) |
| قاعدة البيانات | SQLite محليّة عبر Drift |
| الخدمات السحابية | لا يوجد — التطبيق محلي بالكامل |
| الملف | المحتوى |
|---|---|
CHANGELOG.md |
سجل التغييرات وروابط تحميل كل إصدار |
docs/STORAGE_AND_PERMISSIONS.md |
المجلد العام، MediaStore، منطق الصلاحيات |
docs/INVOICE_FORENSICS.md |
تحليل الفاتورة وقواعد تعديلها الملزمة |
| القناة | العنوان |
|---|---|
| واتساب | +967 770 941 666 |
| الموقع | alabbasi.uk |
الفاتورة مطابقة 100% للنسخة الأصلية (bill.docx / bill.pdf). هذا
ليس هدفاً جمالياً بل شرطاً وظيفياً: الفاتورة مستند رسمي يُقدَّم للمشتركين،
فأي انزياح في موضع أو لون أو مقاس يجعلها مستنداً مختلفاً.
لذا يخضع ملف lib/features/invoices/invoice_pdf.dart لقواعد صارمة:
- كل قياس مشتَقّ ولا يُكتب رقماً سحرياً — ثوابت
_X(المحاور الأفقية) و_Y(المحاور الرأسية) مرتبطة بسلسلة اشتقاق، وارتفاع الصفحة نفسه مشتَقّ من آخر عنصر فيها (_Y.bottomBar). - الألوان لا تُقارَب بالعين — استُخرجت من
bill.docxنفسه (0000FF ظهر 31 مرة ⇒ هو أزرق العنوان والمبلغ معاً). - كل تعديل يُتحقَّق منه بكسليّاً من داخل ملف الـ PDF لا من صورة.
📄 التفاصيل الكاملة والتحليل الجنائي في docs/INVOICE_FORENSICS.md.
عند حفظ فاتورة أو نسخة احتياطية تُكتب الملفات في مجلد عام يراه المستخدم، ويبقى في الجهاز حتى لو أُزيل التطبيق:
Documents/
└── فواتير الكهرباء/
├── الفواتير/
│ └── عبدالله محمد الشامي - 2026-01-05 - 0731.pdf
└── النسخ الاحتياطي/
└── نسخة احتياطية - 2026-01-05 - 2130.json
اسم ملف الفاتورة = اسم المشترك + التاريخ (+ رقم الفاتورة كي لا تتزاحم فاتورتان لنفس المشترك في اليوم نفسه). التاريخ المستخدم هو تاريخ إصدار الفاتورة لا تاريخ اليوم، فيبقى الاسم ثابتاً لو أُعيد الحفظ.
منذ Android 10 (API 29) فُرض التخزين المحدود (Scoped Storage)، فلم
يبقَ للتطبيق حقّ الكتابة عبر مسارات ملفات عادية في الذاكرة المشتركة،
ويرمي النظام SecurityException. الطريق المعتمد رسمياً هو MediaStore
عبر ContentResolver، ولا تُغلّفه أي حزمة Dart بشكل يسمح باختيار
مجلد فرعي داخل Documents — لذلك كُتبت طبقة أصليّة بـ Kotlin:
| الملف | الدور |
|---|---|
android/.../PublicStoragePlugin.kt |
الكتابة عبر MediaStore (API 29+) أو مسار ملف تقليدي (API ≤ 28) |
android/.../MainActivity.kt |
تسجيل MethodChannel |
android/.../res/values/strings.xml |
اسم التطبيق الظاهر على الجهاز — انظر التنبيه أدناه |
lib/core/services/public_storage.dart |
واجهة Dart + إدارة الصلاحية |
⚠️ تنبيه: اسم التطبيق يجب أن يبقى فيstrings.xml. كان التطبيق يظهر على الجهاز باسم "My App" لأنandroid:labelفي المانيفست كان نصاً حرفياً، وأدوات البناء الآلية تعيد كتابته. الحل هو نقل الاسم إلى مورد نصّي:<!-- android/app/src/main/res/values/strings.xml --> <string name="app_name">فواتير الكهرباء</string><!-- AndroidManifest.xml --> <application android:label="@string/app_name" …>لا تُرجع النص الحرفي إلى
android:labelأبداً — سيعود الخطأ.
نقطة مهمة في الصلاحيات: على API 29+ لا يُطلب من المستخدم أي صلاحية لأن التطبيق هو مُنشئ الملف، ولذلك حُدّد في المانيفست:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />فلا يُزعج مستخدمو الأجهزة الحديثة بنافذة صلاحية لا معنى لها، وتُطلب الصلاحية فعلاً فقط على Android 9 وما قبله حيث تكون لازمة. وإن رفض المستخدم تُعرض نافذة تشرح لماذا مع زرّ يفتح إعدادات التطبيق.
المرجع: developer.android.com — Access media files from shared storage
- درج جانبي يُفتح من اليمين (اتجاه التطبيق RTL، فيكون
drawerهو درج البداية تلقائياً بلا حاجة إلىendDrawer): الرئيسية، المشتركون، فاتورة جديدة، أرشيف الفواتير، الإعدادات والنسخ الاحتياطي، المطوّر والتواصل — ويُذيَّل باسم المطوّر ورقم الإصدار. - شريط سفلي عائم بأربعة عناصر وزرّ ذهبي مركزي لإنشاء فاتورة.
- صفحة المطوّر والتواصل (
/about): واتساب، الموقع، اتصال هاتفي (ضغطة طويلة تنسخ)، ومعلومات التطبيق والخصوصية.
url_launcher يفشل بصمت على Android 11+ ما لم يُعلَن عن النوايا في
<queries>، فأُضيفت نوايا https و tel و SEND إلى المانيفست.
lib/
├── app/ theme.dart · router.dart
├── core/
│ ├── app_info.dart ثوابت الهوية والمطوّر — مصدر واحد للحقيقة
│ ├── database/ tables.dart · billing_repository.dart (Drift)
│ ├── providers.dart مزوّدات Riverpod
│ ├── services/
│ │ └── public_storage.dart الحفظ في المجلد العام
│ ├── utils/ formatters · invoice_calculator · arabic_number_words
│ └── widgets/ app_shell (+ الدرج) · luxe_button · luxe_card · …
└── features/
├── about/ صفحة المطوّر والتواصل
├── dashboard/ لوحة التحكم والإحصائيات
├── invoices/ النموذج · الأرشيف · المعاينة · invoice_pdf.dart
├── onboarding/ شاشة البداية · الترخيص
├── settings/ الإعدادات · النسخ الاحتياطي
└── subscribers/ المشتركون
كل نص عن المطوّر أو التواصل أو الإصدار يُقرأ من lib/core/app_info.dart
حصراً. سبب ذلك عملي: نص المطوّر كان مكرَّراً في ملفين، فكان تغييره
يستلزم تعديل كل نسخة على حِدة، ونسيان إحداها يُنتج تطبيقاً يعرض اسمين
مختلفين للمطوّر في شاشتين.
- لا يجمع التطبيق أي بيانات ولا يُرسل شيئاً إلى أي جهة.
- لا يحتاج إنترنت للعمل.
- كل المشتركين والفواتير والإعدادات في قاعدة بيانات محليّة.
- الملفات المُصدَّرة تُكتب في مجلد عام يملكه المستخدم.
flutter pub get
flutter analyze # يجب أن يكون: No issues found!
flutter test # 14 اختباراً
# معاينة ويب
flutter build web --release
# إصدار أندرويد (ثلاث معماريات منفصلة لتقليل الحجم)
flutter build apk --release --split-per-abi \
--obfuscate --split-debug-info=build/symbolsالحزمة الأولى كانت 65 MB، ونزلت إلى ~10 MB لكل معمارية:
| الإجراء | الأثر |
|---|---|
--split-per-abi |
حزمة لكل معمارية بدل حزمة تضمّ الثلاث |
--obfuscate --split-debug-info |
إخراج رموز التنقيح من الحزمة |
useLegacyPackaging = true |
ضغط المكتبات الأصلية |
تقليص الخطوط بـ fontTools |
Amiri: 431KB→291KB، Tinos: 550KB→20KB |
resourceConfigurations = ["ar","en"] |
إسقاط موارد اللغات غير المستخدمة |
flutter test — 14 اختباراً:
- تنسيق الأرقام مطابقاً لـ
toLocaleString('en-US')في نسخة الويب. - معادلات الفاتورة (الاستهلاك، القيمة الأساسية، الإجمالي، الصافي).
- تحويل الأرقام إلى كلمات عربية.
- تنقية أسماء الملفات — تحمي قابلية الحفظ: أي محرف ممنوع في نظام الملفات يجعل الحفظ يفشل بصمت، ولن يلاحظ المستخدم إلا بعد أن يفقد فاتورة.
- أسماء ملفات الفواتير والنسخ الاحتياطية.
كما يوجد مِسبار تحقّق بصري: test/pdf_render_probe.dart يُخرج فاتورة
حقيقية إلى /tmp/probe/out.pdf لتفحّصها بكسليّاً.