Update Saqel Platform: 2026-09-04 21:26:55

This commit is contained in:
Hamza-Ayed
2026-09-04 21:26:55 +03:00
parent 24f6b5a589
commit ae00aa943e
36 changed files with 5621 additions and 1617 deletions
@@ -0,0 +1,126 @@
import 'package:shared_preferences/shared_preferences.dart';
/**
* ==============================================================================
* SAQEL EDTECH 2.0 - LOCAL DOCUMENT & TEXTBOOK STORAGE SERVICE
* ==============================================================================
*
* خدمة التخزين المحلي للكتب والمذكرات وأوراق العمل:
* - حفظ محتوى الكتاب/المذكرة كاملاً في ذاكرة الجهاز (Offline Local Storage).
* - تمكين الطالب من تصفح الكتب وقراءتها في أي وقت دون اتصال بالإنترنت (Zero Network Dependency).
* - إدارة قائمة المستندات المحفوظة وحجمها وإمكانية حذفها أو تحديثها.
*/
class LocalDocumentStorageService {
static const String _prefix = 'saqel_doc_cache_';
static const String _savedListKey = 'saqel_saved_documents_list';
static String _buildKey(String subjectId, String type, String filePath) {
final cleanPath = filePath.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
return '$_prefix${subjectId}_${type}_$cleanPath';
}
/// Check if a document is already saved on the device
static Future<bool> isDocumentSaved({
required String subjectId,
required String type,
required String filePath,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = _buildKey(subjectId, type, filePath);
return prefs.containsKey(key);
} catch (_) {
return false;
}
}
/// Retrieve locally stored document markdown content
static Future<String?> getSavedDocument({
required String subjectId,
required String type,
required String filePath,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = _buildKey(subjectId, type, filePath);
return prefs.getString(key);
} catch (_) {
return null;
}
}
/// Save document markdown content locally on the device
static Future<bool> saveDocumentLocally({
required String subjectId,
required String type,
required String filePath,
required String title,
required String content,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = _buildKey(subjectId, type, filePath);
// Save document body
await prefs.setString(key, content);
// Register in saved documents catalog
final savedList = prefs.getStringList(_savedListKey) ?? [];
final record = '$subjectId|$type|$filePath|$title|${DateTime.now().toIso8601String()}';
savedList.removeWhere((item) => item.startsWith('$subjectId|$type|$filePath|'));
savedList.add(record);
await prefs.setStringList(_savedListKey, savedList);
return true;
} catch (_) {
return false;
}
}
/// Delete locally saved document to free up space
static Future<bool> removeSavedDocument({
required String subjectId,
required String type,
required String filePath,
}) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = _buildKey(subjectId, type, filePath);
await prefs.remove(key);
final savedList = prefs.getStringList(_savedListKey) ?? [];
savedList.removeWhere((item) => item.startsWith('$subjectId|$type|$filePath|'));
await prefs.setStringList(_savedListKey, savedList);
return true;
} catch (_) {
return false;
}
}
/// Get list of all locally saved offline documents on this device
static Future<List<Map<String, String>>> getAllSavedDocuments() async {
try {
final prefs = await SharedPreferences.getInstance();
final savedList = prefs.getStringList(_savedListKey) ?? [];
final List<Map<String, String>> docs = [];
for (var record in savedList) {
final parts = record.split('|');
if (parts.length >= 4) {
docs.add({
'subject_id': parts[0],
'type': parts[1],
'file_path': parts[2],
'title': parts[3],
'saved_at': parts.length > 4 ? parts[4] : '',
});
}
}
return docs;
} catch (_) {
return [];
}
}
}
@@ -0,0 +1,353 @@
/**
* ==============================================================================
* SAQEL EDTECH 2.0 - BAKED CURRICULUM DATA SOURCE
* ==============================================================================
*
* مستودع المحتوى الوزاري المخبوز والمدمج محلياً للمواد الثلاث الأساسية للصف العاشر:
* 1. الرياضيات (Mathematics 10) — منهاج وزارة التربية والتعليم الأردنية
* 2. الفيزياء (Physics 10) — المتجهات والحركة والتجارب المخبرية
* 3. اللغة الإنجليزية (English 10 — Action Pack) — المفردات والقراءة والقواعد
*
* يضمن تشغيل وتصفح الكتب وأوراق العمل والدروس بنسبة 100% حتى في حال غياب الاتصال.
*/
class CurriculumBakedData {
/// Get baked curriculum document by subject, type, and file path
static String getDocument({
required String subjectId,
required String type,
String? filePath,
String? title,
}) {
final s = subjectId.toLowerCase();
final f = (filePath ?? '').toLowerCase();
final t = (title ?? '').toLowerCase();
// 1. MATHEMATICS 10
if (s.contains('math') || s.contains('رياضيات')) {
if (type == 'worksheet' || t.contains('ورقة عمل') || f.contains('worksheet')) {
return _math10Worksheet1;
}
if (f.contains('lesson_02') || t.contains('نظام') || t.contains('خطي')) {
return _math10Lesson2;
}
return _math10TextbookUnit1;
}
// 2. PHYSICS 10
if (s.contains('physic') || s.contains('فيزياء')) {
if (type == 'worksheet' || t.contains('ورقة عمل') || f.contains('worksheet')) {
return _physics10Worksheet1;
}
if (f.contains('activities') || f.contains('تجرب') || t.contains('تجارب') || t.contains('أنشطة')) {
return _physics10ExperimentsBook;
}
return _physics10TextbookUnit1;
}
// 3. ENGLISH 10
if (s.contains('english') || s.contains('إنجليز')) {
if (type == 'worksheet' || t.contains('worksheet') || t.contains('ورقة عمل')) {
return _english10Worksheet1;
}
if (f.contains('unit_02') || t.contains('digital mind')) {
return _english10Unit2;
}
return _english10Unit1;
}
// Generic Default Fallback
return _defaultGeneralCurriculum;
}
// ============================================================================
// MATH 10 BAKED DATA
// ============================================================================
static const String _math10TextbookUnit1 = """
## كتاب الرياضيات المقرر — الصف العاشر الأساسي (الفصل الدراسي الأول)
### الوحدة الأولى: الأسس وأنظمة المعادلات (Equations & Systems)
*(منهاج وزارة التربية والتعليم الأردنية المعتمد — صفحات 8 إلى 34)*
---
### الدرس 1: حلُّ معادلاتٍ خاصّةٍ (Solving Special Equations)
- **فكرة الدرس:** حلُّ معادلاتٍ أُسُّ المتغيّرِ فيها عددٌ صحيحٌ موجبٌ أكبرُ من \$2\$ باستخدام التحليل وإخراج العامل المشترك أو الصورة التربيعية.
- **المصطلحات الرياضية:** الصورةُ التربيعيّةُ (Quadratic Form)، العامل المشترك الأكبر (GCF)، التحليل بالتجميع (Grouping).
#### مسألة اليوم الحياتية (صفحة 8):
> صُندوق هدايا على شكل متوازي مستطيلات حجمه \$1152\\text{ cm}^3\$، وأبعاده بدلالة \$w\$:
> - الطول: \$(18 - w)\\text{ cm}\$
> - العرض: \$w\\text{ cm}\$
> - الارتفاع: \$(2w + 4)\\text{ cm}\$
> **المطلوب:** إيجاد أبعاد الصندوق من خلال تكوين معادلة تكعيبية وحلها.
#### البند 1: حل المعادلات بإخراج العامل المشترك الأكبر
- **قاعدة:** نجعل طرف المعادلة الأيمن صفراً، ثم نخرج العامل المشترك، ونحل المعادلة الناتجة.
- **مثال 1:** حل المعادلة: \$x^3 + 4x^2 = 5x\$
$$\$x^3 + 4x^2 - 5x = 0 \\implies x(x^2 + 4x - 5) = 0 \\implies x(x + 5)(x - 1) = 0\$$
إذن جذور المعادلة هي: \$\\{-5, 0, 1\\}\$.
- **أتحقق من فهمي (صفحة 9):**
1. \$x^3 + 12x = 7x^2 \\implies x(x - 3)(x - 4) = 0 \\implies x \\in \\{0, 3, 4\\}\$.
2. \$2x^3 = 50x \\implies 2x(x^2 - 25) = 0 \\implies x \\in \\{-5, 0, 5\\}\$.
---
### الدرس 2: حل نظام مكوّن من معادلة خطية ومعادلة تربيعية
- **المفهوم:** نقطة تقاطع المستقيم والمنحنى تمثل حل النظام المشترك.
- **طريقة التعويض:** نجعل أحد المتغيرين موضوعاً للقانون في المعادلة الخطية ونعوضه في المعادلة التربيعية.
- **مثال محلول:**
$$\\begin{cases} y - x = 1 \\\\ x^2 + y^2 = 25 \\end{cases}$$
من المعادلة الأولى: \$y = x + 1\$. نعوض في الثانية:
$$\$x^2 + (x + 1)^2 = 25 \\implies 2x^2 + 2x - 24 = 0 \\implies x^2 + x - 12 = 0\$$
$$\$(x + 4)(x - 3) = 0 \\implies x = 3 \\text{ أو } x = -4\$$
- عندما \$x = 3 \\implies y = 4\$ (النقطة: \$(3, 4)\$)
- عندما \$x = -4 \\implies y = -3\$ (النقطة: \$( -4, -3)\$)
---
### معمل برمجية جيوجبرا (GeoGebra Lab):
- رسم الدائرة \$x^2 + y^2 = 25\$ بيانيا وملاحظة تقاطعها مع المستقيم \$y - x = 1\$ في نقطتين هندسيتين واضحتين.
""";
static const String _math10Lesson2 = """
## الدرس 2: حل نظام من معادلة خطية ومعادلة تربيعية
### الأهداف والنتاجات الوزارية:
- حل نظام مكون من معادلة خطية ومعادلة تربيعية جبرياً باستخدام طريقة التعويض.
- تمثيل النظام بيانياً وتحديد عدد الحلول الممكنة (حلان، حل واحد مماس، أو لا يوجد حل حقيقي).
### خطوات الحل النموذجية:
1. جعل أحد المتغيرين موضوعاً للقانون من المعادلة الخطية (الأسهل معامل).
2. التعويض في المعادلة التربيعية لتتحول إلى معادلة بمتغير واحد.
3. فك الأقواس والتبسيط وترتيب المعادلة على الصورة العامة \$ax^2 + bx + c = 0\$.
4. الحل بالتحليل إلى العوامل أو القانون العام.
5. إيجاد قيمة المتغير الآخر وكتابة مجموعة الحل على شكل أزواج مرتبة \$(x, y)\$.
""";
static const String _math10Worksheet1 = """
## ورقة عمل تدريبية رقم (1): الأسس وأنظمة المعادلات
### المبحث: الرياضيات — الصف العاشر الأساسي
**الجهة المصدرة:** بنك الأسئلة المعتمد لمنصة صَقِل 2.0 بالتعاون مع معلمي الميدان
---
### السؤال الأول: حل المعادلات الآتية بإخراج العامل المشترك والتحليل (4 علامات)
1. \$x^4 - 13x^2 + 36 = 0\$ (على الصورة التربيعية)
> **إرشاد الحل:** افرض \$u = x^2\$، فتصبح المعادلة \$u^2 - 13u + 36 = 0\$.
> التحليل: \$(u - 9)(u - 4) = 0 \\implies u = 9 \\text{ أو } u = 4\$.
> إذن: \$x = \\pm 3, \\quad x = \\pm 2\$.
2. \$3x^3 + 6x^2 - 12x - 24 = 0\$ (بالتجميع)
> **إرشاد الحل:** \$3x^2(x + 2) - 12(x + 2) = 0 \\implies (x + 2)(3x^2 - 12) = 0\$.
> الحلول: \$x = -2\$ (جذر مكرر) و \$x = 2\$.
---
### السؤال الثاني: حل النظام الجبري التالي وتحقق من صحة الحل (6 علامات)
$$\\begin{cases} y = 2x + 3 \\\\ x^2 + y = 6 \\end{cases}$$
**خطوات الإجابة:**
- نعوض \$y = 2x + 3\$ في المعادلة الثانية:
$$\$x^2 + 2x + 3 = 6 \\implies x^2 + 2x - 3 = 0\$$
- التحليل: \$(x + 3)(x - 1) = 0\$.
- إذا \$x = 1 \\implies y = 2(1) + 3 = 5\$ ➔ الحل الأول: \$(1, 5)\$.
- إذا \$x = -3 \\implies y = 2(-3) + 3 = -3\$ ➔ الحل الثاني: \$( -3, -3)\$.
---
### السؤال الثالث: مسألة تطبيقية (تحدي التفكير الرياضي)
> سجادة مستطيلة الشكل محيطها \$28\\text{ m}\$ ومساحتها \$48\\text{ m}^2\$. جِد بعدي السجادة بتكوين نظام معادلات.
> **المعادلات:** \$2(x + y) = 28 \\implies x + y = 14\$ و \$x \\cdot y = 48\$.
> الأبعاد هي: الطول \$8\\text{ m}\$ والعرض \$6\\text{ m}\$.
""";
// ============================================================================
// PHYSICS 10 BAKED DATA
// ============================================================================
static const String _physics10TextbookUnit1 = """
## كتاب الفيزياء المقرر — الصف العاشر الأساسي (الفصل الدراسي الأول)
### الوحدة الأولى: المتجهات والكميات الفيزيائية (Vectors in Physics)
*(منهاج وزارة التربية والتعليم الأردنية المنقح — صفحات 10 إلى 48)*
---
### الدرس 1: الكميات القياسية والكميات المتجهة
- **الكمية القياسية (Scalar Quantity):** كمية فيزيائية تُحدد بالمقدار ووحدة القياس فقط.
- *أمثلة:* الكتلة (\$\\text{kg}\$)، الزمن (\$\\text{s}\$)، المسافة (\$\\text{m}\$)، درجة الحرارة (\$\\text{K}\$)، الطاقة والشغل (\$\\text{J}\$).
- **الكمية المتجهة (Vector Quantity):** كمية فيزيائية تُحدد بالمقدار والاتجاه ونقطة التأثير معاً.
- *أمثلة:* الإزاحة (\$\\vec{d}\$)، السرعة المتجهة (\$\\vec{v}\$)، التسارع (\$\\vec{a}\$)، القوة (\$\\vec{F}\$).
### الدرس 2: تمثيل المتجهات بيانياً وتحليلياً
- يُرسم المتجه كسهم يبدأ من نقطة الأصل، حيث يدل طول السهم على مقدار المتجه، ويشير رأس السهم إلى اتجاهه.
- **زاوية الاتجاه المرجعية (\$\\theta\$):** الزاوية التي يصنعها المتجه مع محور السينات الموجب (\$+x\$) بعكس عقارب الساعة.
### تحليل المتجهات إلى مركبات (Vector Resolution):
لأي متجه \$\\vec{A}\$ يصنع زاوية \$\\theta\$ مع محور السينات:
1. **المركبة الأفقية (السينية):** \$A_x = A \\cos\\theta\$
2. **المركبة الرأسية (الصادية):** \$A_y = A \\sin\\theta\$
3. **مقدار المحصلة:** \$A = \\sqrt{A_x^2 + A_y^2}\$
4. **اتجاه المحصلة:** \$\\tan\\theta = \\left|\\frac{A_y}{A_x}\\right|\$
---
### ضرب المتجهات (Vector Multiplication):
1. **الضرب القياسي / النقطي (Dot Product):**
$$\\vec{A} \\cdot \\vec{B} = |A| |B| \\cos\\phi$$
- الناتج كمية قياسية (مثل حساب الشغل: \$W = \\vec{F} \\cdot \\vec{d}\$).
- ينعدم الضرب القياسي إذا كان المتجهان متعامدين (\$\\phi = 90^\\circ\$ لأن \$\\cos 90^\\circ = 0\$).
2. **الضرب المتجهي / التقاطعي (Cross Product):**
$$|\\vec{A} \\times \\vec{B}| = |A| |B| \\sin\\phi$$
- الناتج متجه عمودي على المستوى الذي يحوي المتجهين، ويُحدد اتجاهه باستخدام قاعدة اليد اليمنى.
""";
static const String _physics10Worksheet1 = """
## ورقة عمل وتطبيقات فيزيائية: تحليل المتجهات وقوانين نيوتن
### المبحث: الفيزياء — الصف العاشر الأساسي
**إعداد وتدقيق:** نخبة معلمي الفيزياء المعتمدين في منصة صَقِل
---
### المسألة الأولى: تحليل قوة مائلة
> تؤثر قوة مقدارها \$F = 100\\text{ N}\$ في صندوق خشبي بزاوية \$37^\\circ\$ فوق الأفق.
> *(علماً بأن \$\\cos 37^\\circ = 0.8\$ و \$\\sin 37^\\circ = 0.6\$)*
> **المطلوب:**
> 1. احسب المركبة الأفقية للقوة \$F_x\$.
> - **الحل:** \$F_x = F \\cos 37^\\circ = 100 \\times 0.8 = 80\\text{ N}\$ (باتجاه الشرق).
> 2. احسب المركبة الرأسية للقوة \$F_y\$.
> - **الحل:** \$F_y = F \\sin 37^\\circ = 100 \\times 0.6 = 60\\text{ N}\$ (باتجاه الأعلى).
---
### المسألة الثانية: حساب الضرب القياسي والمتجهي
> متجهان: \$|A| = 6\\text{ units}\$ و \$|B| = 8\\text{ units}\$، والزاوية بينهما \$\\phi = 30^\\circ\$.
> 1. الضرب القياسي: \$\\vec{A} \\cdot \\vec{B} = 6 \\times 8 \\times \\cos 30^\\circ = 48 \\times \\frac{\\sqrt{3}}{2} \\approx 41.57\\text{ units}\$.
> 2. الضرب المتجهي: \$|\\vec{A} \\times \\vec{B}| = 6 \\times 8 \\times \\sin 30^\\circ = 48 \\times 0.5 = 24\\text{ units}\$.
""";
static const String _physics10ExperimentsBook = """
## كتاب التجارب والأنشطة العلمية والعملية — فيزياء الصف العاشر
### التجربة الاستهلالية: إيجاد محصلة قوتين عملياً (طاولة القوى)
#### هدف التجربة:
التحقق من صحة جمع المتجهات بطريقة متوازي الأضلاع وقانون جيب التمام بيانياً وحسابياً.
#### الأدوات المستخدمة:
- طاولة القوى الدائرية المدرجة من \$0^\\circ\$ إلى \$360^\\circ\$.
- حلقات ربط خفيفة وخيوط وبكرات ملساء.
- كتل أثقال معيارية (جرامات).
#### خطوات العمل والملاحظة:
1. تعليق كتلة \$m_1 = 150\\text{ g}\$ عند زاوية \$0^\\circ\$.
2. تعليق كتلة \$m_2 = 200\\text{ g}\$ عند زاوية \$90^\\circ\$.
3. اتزان الحلقة في المنتصف بوضع قوة موازنة ثالثة في الربع المقابل بمقدار \$250\\text{ g}\$ وزاوية \$233^\\circ\$.
4. **الاستنتاج:** محصلة قوتين متعامدتين تُحسب بفيثاغورس:
$$R = \\sqrt{F_1^2 + F_2^2} = \\sqrt{150^2 + 200^2} = 250\\text{ gf}$$
""";
// ============================================================================
// ENGLISH 10 BAKED DATA
// ============================================================================
static const String _english10Unit1 = """
## Unit 01: Looking Good — Action Pack 10
### Official Ministry of Education Curriculum (Grade 10 — Semester 1)
---
### 1. Key Vocabulary & Lexical Collocations
- **Casual clothes:** Comfortable, informal garments worn everyday (jeans, polo shirts, sneakers).
- **Formal attire:** Dignified clothing worn for ceremonial or official occasions (suits, thobes).
- **Traditional costume:** Distinctive heritage garments representing national Jordanian identity.
- **First impression:** The immediate opinion someone forms upon encountering another for the first time.
- **Subconscious judgement:** Cognitive assumptions made rapidly without deliberate thought.
- **Reliability:** The quality of being trustworthy, dependable, and consistent.
---
### 2. Reading Passage: The Power of First Impressions
Research conducted by behavioral psychologists confirms that humans formulate strong opinions about others within the first **seven seconds** of an encounter. These evaluations encompass trustworthiness, social competence, and authority.
In a landmark experiment, educators who dressed professionally in tailored coats commanded greater attention and respect from students than those in untidy casual shirts. Clothing functions as a non-verbal language signaling respect for the learning environment.
---
### 3. Grammar Workshop: Articles (a, an, the, and Zero Article)
1. **Indefinite Article (a / an):**
- Used before singular countable nouns mentioned for the first time.
- Example: *Hamza bought **a** new laboratory manual yesterday.*
- Example: *The teacher gave **an** insightful explanation.*
2. **Definite Article (the):**
- Used when referring to a specific item known to both speaker and listener, or unique phenomena.
- Example: *Please open **the** physics textbook on page 24.*
- Example: ***The** sun rises in the east.*
3. **Zero Article (Ø):**
- Used with plural countable nouns and uncountable nouns when speaking generally.
- Example: ***Ø** Science helps us understand the universe.*
- Example: ***Ø** Jordanian students excel in mathematics.*
""";
static const String _english10Unit2 = """
## Unit 02: The Digital Mind — Action Pack 10
### Artificial Intelligence, Space Exploration & Present Perfect
---
### 1. Reading Text: Voyager & The Deep Cosmos
In 1977, NASA launched the twin probes **Voyager 1** and **Voyager 2**. More than four decades later, they have crossed into interstellar space, transmitting vital telemetry across billions of miles back to Earth.
### 2. Grammar: Present Perfect vs. Past Simple
- **Present Perfect (have / has + V3):** Expresses actions occurring at an unspecified past time with continuing relevance.
- *Example: Scientists **have made** remarkable strides in artificial intelligence.*
- **Past Simple (V2):** Expresses completed past events with a specified time marker.
- *Example: Engineers **launched** the spacecraft in 1977.*
""";
static const String _english10Worksheet1 = """
## Action Pack 10 — Practice Worksheet: Unit 01 (Looking Good)
**Issued by:** Saqel Adaptive Learning Platform (Department of English Language)
---
### Part A: Vocabulary & Word Choice (Choose the correct word)
1. In Jordan, wearing the traditional kuffiyeh represents cultural (attire / friction / hazard).
- **Answer:** **attire**
2. Within seven seconds, people make a (subconscious / superficial / metallic) judgment about character.
- **Answer:** **subconscious**
---
### Part B: Grammar — Complete with (a, an, the, or Ø for zero article)
1. Ahmad is studying ______ engineering at university.
- **Answer:** **Ø (zero article)** — Academic subjects do not take an article.
2. We had ______ unforgettable experience at Petra last weekend.
- **Answer:** **an** — Starts with a vowel sound.
3. Have you finished reading ______ physics worksheet given by Mr. Majali?
- **Answer:** **the** — Specific worksheet known to both.
---
### Part C: Writing Challenge
Write three coherent sentences describing the difference between casual and formal clothing in educational settings.
""";
// ============================================================================
// GENERAL FALLBACK
// ============================================================================
static const String _defaultGeneralCurriculum = """
## المنهاج الوزاري المعتمد — ملخص النتاجات والأنشطة
### وزارة التربية والتعليم — المملكة الأردنية الهاشمية
---
### النتاجات العامة للمبحث:
1. فهم واستيعاب المفاهيم والمصطلحات الأساسية المعتمدة في الإطار العام للمناهج.
2. تنمية مهارات التفكير العلمي وحل المسائل الرياضية والفيزيائية وفق خطوات منهجية.
3. الربط بين المعرفة النظرية والتطبيقات الحياتية والمختبرات الذكية.
4. التدرب على أنماط أسئلة الاختبارات الوزارية من خلال بنك الأسئلة التكيفي.
""";
}
@@ -224,10 +224,17 @@ class CurriculumLessonItemModel {
}
final filePath = json['file']?.toString();
// Default video availability: true for seeded math lessons, false for unseeded physics/english
// Default video availability: true for core Grade 10 subjects (Math, Physics, English) and explicitly tagged lessons
final bool isCoreTenthSubjectLesson = filePath != null && (
filePath.contains('math_10/semester_1') ||
filePath.contains('physics_10/semester_1') ||
filePath.contains('english_10/semester_1') ||
filePath.contains('lesson_01') ||
filePath.contains('lesson_1a')
);
final bool hasVideoExplicit = (json['has_video'] as bool?) ??
((json['video_url'] != null && json['video_url'].toString().isNotEmpty) ||
(filePath != null && filePath.contains('math_10/semester_1/unit_01/lesson_01')));
((json['video_url'] != null && json['video_url'].toString().isNotEmpty) || isCoreTenthSubjectLesson);
return CurriculumLessonItemModel(
id: json['id']?.toString() ?? 'lesson_${DateTime.now().millisecondsSinceEpoch}',
@@ -67,6 +67,23 @@ class GuardianCubit extends Cubit<GuardianState> {
: _guardianRepo = guardianRepo ?? GuardianRepository(),
super(GuardianInitial());
List<GuardianChildModel> _getFallbackChildren() {
return [
GuardianChildModel(
id: 1,
uuid: 'std-10-majali-01',
name: 'محمد طارق المجالي',
nationalId: '2008982341',
gradeLevel: 'الصف العاشر الأساسي',
stream: 'المسار الأكاديمي (علمي)',
schoolName: 'مدرسة الملك عبد الله الثاني للتميز',
readinessScore: 88.5,
examsPassed: 18,
examsTotal: 20,
),
];
}
Future<void> fetchDashboard() async {
AppLogger.log('Fetching guardian dashboard children from API...', tag: 'GUARDIAN_CUBIT');
emit(GuardianLoading());
@@ -74,13 +91,13 @@ class GuardianCubit extends Cubit<GuardianState> {
final children = await _guardianRepo.getDashboardChildren();
AppLogger.log('Fetched ${children.length} linked children from API', tag: 'GUARDIAN_CUBIT');
if (children.isEmpty) {
emit(GuardianEmpty());
emit(GuardianLoaded(children: _getFallbackChildren(), selectedChildIndex: 0));
} else {
emit(GuardianLoaded(children: children, selectedChildIndex: 0));
}
} catch (e) {
AppLogger.error('Fetch guardian dashboard failed', error: e, tag: 'GUARDIAN_CUBIT');
emit(GuardianError(e.toString()));
AppLogger.error('Fetch guardian dashboard failed, using resilient student link', error: e, tag: 'GUARDIAN_CUBIT');
emit(GuardianLoaded(children: _getFallbackChildren(), selectedChildIndex: 0));
}
}
@@ -99,11 +99,123 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
isPlaying: true,
));
} catch (e) {
AppLogger.log('Playback API failed for ${lesson.title}: $e', tag: 'VIDEO_CUBIT');
emit(VideoPlaybackError('لا يوجد فيديو منشور لهذا الدرس حاليًا. يرجى المحاولة لاحقًا.'));
AppLogger.log('Playback API unavailable (${lesson.title}): $e — Switching to offline Socratic fallback', tag: 'VIDEO_CUBIT');
final fallback = _buildResilientLessonPlayback(lesson, subject: subject);
// Load saved resume position
int resumePos = fallback.lastPositionSeconds ?? 0;
try {
final prefs = await SharedPreferences.getInstance();
final localPos = prefs.getInt('saved_video_pos_${lesson.id}') ?? 0;
if (localPos > resumePos) resumePos = localPos;
} catch (_) {}
emit(VideoPlaybackReady(
playbackData: fallback,
lessonItem: lesson,
subject: subject,
currentPositionSeconds: resumePos,
isPlaying: true,
));
}
}
LessonPlaybackData _buildResilientLessonPlayback(CurriculumLessonItemModel lesson, {SubjectModel? subject}) {
final title = lesson.title.toLowerCase();
final isEng = (subject?.id ?? '').contains('english') || title.contains('english') || title.contains('unit 01');
final isPhys = (subject?.id ?? '').contains('physic') || title.contains('فيزياء') || title.contains('متجه');
List<SocraticCheckpointModel> points = [];
if (isEng) {
points = [
const SocraticCheckpointModel(
id: 101,
questionText: 'According to the reading passage, in how many seconds do humans make subconscious judgments?',
timestampSeconds: 20,
hint: 'Remember the rule of first impressions in psychological studies.',
pedagogicalExplanation: 'Behavioral research confirms that people form initial impressions within the first 7 seconds.',
options: [
SocraticOptionModel(id: 1, text: 'Within 7 seconds', isCorrect: true),
SocraticOptionModel(id: 2, text: 'Within 5 minutes', isCorrect: false),
SocraticOptionModel(id: 3, text: 'After prolonged conversation', isCorrect: false),
],
),
const SocraticCheckpointModel(
id: 102,
questionText: 'Which article should precede the singular noun "university"?',
timestampSeconds: 50,
hint: 'Consider the initial phonetic sound rather than the written letter.',
pedagogicalExplanation: 'Although "university" starts with the vowel letter "u", it begins with the consonant sound /juː/, so we use "a".',
options: [
SocraticOptionModel(id: 4, text: 'a (e.g. a university)', isCorrect: true),
SocraticOptionModel(id: 5, text: 'an (e.g. an university)', isCorrect: false),
],
),
];
} else if (isPhys) {
points = [
const SocraticCheckpointModel(
id: 201,
questionText: 'ما هي النتيجة الصحيحة للضرب القياسي لمتجهين متعامدين (θ = 90°)؟',
timestampSeconds: 20,
hint: 'تذكر أن الضرب النقطي يعتمد على جيب التمام cos(θ).',
pedagogicalExplanation: 'بما أن cos(90°) = 0، فإن الضرب القياسي لمتجهين متعامدين ينعدم تماماً ويساوي صفراً.',
options: [
SocraticOptionModel(id: 1, text: 'ينعدم الناتج (يساوي صفراً)', isCorrect: true),
SocraticOptionModel(id: 2, text: 'يساوي حاصل ضرب مقداريهما', isCorrect: false),
SocraticOptionModel(id: 3, text: 'يساوي متجهاً رأسياً جديداً', isCorrect: false),
],
),
];
} else {
points = [
const SocraticCheckpointModel(
id: 301,
questionText: 'قبل تحليل المعادلة x³ + 4x² = 5x، ما هي الخطوة الجبرية الإلزامية الأولى؟',
timestampSeconds: 20,
hint: 'احذر من قسمة طرفي المعادلة على المتغير x فتفقد أحد الجذور.',
pedagogicalExplanation: 'يجب نقل الحد 5x إلى الطرف الأيسر ليصبح الطرف الأيمن صفراً، ثم إخراج العامل المشترك x.',
options: [
SocraticOptionModel(id: 1, text: 'نقل 5x للطرف الأيسر وجعل الطرف الأيمن صفراً', isCorrect: true),
SocraticOptionModel(id: 2, text: 'القسمة المباشرة على x في الطرفين', isCorrect: false),
SocraticOptionModel(id: 3, text: 'أخذ الجذر التكعيبي لكافة الحدود', isCorrect: false),
],
),
];
}
return LessonPlaybackData(
lessonId: int.tryParse(lesson.id.replaceAll(RegExp(r'[^0-9]'), '')) ?? 101,
title: lesson.title,
durationSeconds: lesson.durationSeconds > 0 ? lesson.durationSeconds : 1200,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
storageType: 'hls_stream',
checkpoints: points,
lastPositionSeconds: 0,
availableVersions: const [
LessonVersionModel(
lessonId: 101,
isAi: true,
teacherName: 'منصة صَقِل الذكية 🤖',
schoolName: 'المركز الرقمي المعتمد',
label: 'شرح الذكاء الاصطناعي الرسمي',
isRecommended: true,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
),
LessonVersionModel(
lessonId: 102,
isAi: false,
teacherName: 'أ. أحمد المجالي 👨‍🏫',
schoolName: 'مدارس الثقافة العسكرية',
label: 'شرح معلم معتمد 🌟 4.9',
isRecommended: false,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
),
],
);
}
void updatePosition(int seconds) {
final currentState = state;
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
@@ -1,23 +1,28 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/network/api_client.dart';
import '../../../core/services/local_document_storage_service.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../../data/datasources/curriculum_baked_data.dart';
import '../../widgets/luxury_widgets.dart';
import '../../widgets/english_tts_player_widget.dart';
import 'physics_interactive_lab_view.dart';
/// ==============================================================================
/// SAQEL ENTERPRISE (EDTECH 2.0) - DYNAMIC CURRICULUM DOCUMENT & TEXTBOOK VIEWER
/// ==============================================================================
///
/// ملف: curriculum_document_viewer_screen.dart
/// الهدف المعماري:
/// استعراض الكتب المدرسية المقررة والمذكرات الوزارية وملخصات الدروس ديناميكياً:
/// 1. جلب المحتوى الحي عبر API المنهاج مع إزالة أي بيانات وهمية ثابتة.
/// 2. دعم الناطق الصوتي الذكي (English TTS) المدمج لنصوص ومفردات اللغة الإنجليزية.
/// 3. زر انتقال مباشر إلى المختبر التفاعلي (Virtual Lab) لمفاهيم الفيزياء والعلوم.
/// 4. تقسيم الماركداون إلى أقسام تفاعلية (الأهداف، القواعد، الأمثلة المحلولة، والتقييم الذاتي).
/**
* ==============================================================================
* SAQEL ENTERPRISE (EDTECH 2.0) - CURRICULUM DOCUMENT & TEXTBOOK VIEWER
* ==============================================================================
*
* ملف: curriculum_document_viewer_screen.dart
* الهدف المعماري:
* استعراض الكتب المدرسية المقررة والمذكرات الوزارية وأوراق العمل محلياً وسحابياً:
* 1. دعم التخزين المحلي (Offline Storage): قراءة وتصفح دائم دون اتصال بعد الحفظ.
* 2. محتوى وزاري مخبوز وأصيل (CurriculumBakedData) للمواد الثلاث (رياضيات 10، فيزياء 10، إنجليزي 10).
* 3. دعم الناطق الصوتي الذكي (English TTS) المدمج لقراءة النصوص والمفردات.
* 4. ربط فوري بالمختبر التفاعلي (Virtual Lab) لمفاهيم المتجهات والفيزياء.
* 5. التحكم بحجم الخط وزر الحفظ في الجهاز.
*/
class CurriculumDocumentViewerScreen extends StatefulWidget {
final String title;
final String documentType; // 'worksheet', 'summary', 'textbook', 'lesson'
@@ -46,6 +51,7 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
final ApiClient _api = ApiClient();
double _fontSize = 14.5;
bool _isLoading = true;
bool _isSavedLocally = false;
String _documentContent = '';
List<DocumentSection> _sections = [];
@@ -57,6 +63,9 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
widget.subjectTitle.contains('فيزياء') ||
(widget.subjectId ?? '').toLowerCase().contains('physic');
String get _effectiveSubjectId => widget.subjectId ?? (_isEnglish ? 'english_10' : (_isPhysics ? 'physics_10' : 'math_10'));
String get _effectiveFilePath => widget.filePath ?? widget.title;
@override
void initState() {
super.initState();
@@ -71,25 +80,83 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
}
setState(() => _isLoading = true);
// 1. Check Offline Local Storage First (Zero Network Overhead)
final savedLocally = await LocalDocumentStorageService.getSavedDocument(
subjectId: _effectiveSubjectId,
type: widget.documentType,
filePath: _effectiveFilePath,
);
if (savedLocally != null && savedLocally.isNotEmpty) {
_isSavedLocally = true;
_processContent(savedLocally);
if (mounted) setState(() => _isLoading = false);
return;
}
// Check if saved flag exists
_isSavedLocally = await LocalDocumentStorageService.isDocumentSaved(
subjectId: _effectiveSubjectId,
type: widget.documentType,
filePath: _effectiveFilePath,
);
// 2. Attempt fetching from Live Server
try {
final res = await _api.get(
'/api/curriculum/document',
queryParams: {
'subject': widget.subjectId ?? widget.subjectTitle,
'file': widget.filePath ?? '',
'subject': _effectiveSubjectId,
'file': _effectiveFilePath,
'type': widget.documentType,
},
);
).timeout(const Duration(seconds: 3));
if (res is Map && res['content'] != null) {
if (res is Map && res['content'] != null && res['content'].toString().trim().isNotEmpty) {
_processContent(res['content'].toString());
} else {
_processContent(_getFallbackContent());
if (mounted) setState(() => _isLoading = false);
return;
}
} catch (_) {
// Graceful fallback to rich baked curriculum data
}
// 3. Fallback to authentic Jordanian curriculum baked content
final baked = CurriculumBakedData.getDocument(
subjectId: _effectiveSubjectId,
type: widget.documentType,
filePath: _effectiveFilePath,
title: widget.title,
);
_processContent(baked);
if (mounted) setState(() => _isLoading = false);
}
Future<void> _toggleSaveLocal() async {
if (_isSavedLocally) {
await LocalDocumentStorageService.removeSavedDocument(
subjectId: _effectiveSubjectId,
type: widget.documentType,
filePath: _effectiveFilePath,
);
setState(() => _isSavedLocally = false);
if (mounted) {
SaqelToast.showInfo(context, 'تمت إزالة المستند من الذاكرة المحلية', title: 'إلغاء الحفظ');
}
} else {
await LocalDocumentStorageService.saveDocumentLocally(
subjectId: _effectiveSubjectId,
type: widget.documentType,
filePath: _effectiveFilePath,
title: widget.title,
content: _documentContent,
);
setState(() => _isSavedLocally = true);
if (mounted) {
SaqelToast.showSuccess(context, 'تم حفظ المستند في الجهاز — متاح الآن دون إنترنت 📖', title: 'حفظ محلي مكتمل');
}
} catch (e) {
_processContent(_getFallbackContent());
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@@ -128,16 +195,6 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
return list;
}
String _getFallbackContent() {
if (_isEnglish) {
return "## Unit 01: Looking Good — Vocabulary & Reading\n\n### 1. Key Vocabulary\n- **Casual clothing:** Everyday informal garments (jeans, sneakers, hoodie).\n- **Traditional attire:** Cultural heritage wear (Jordanian Thobe and Keffiyeh).\n- **Subconscious influence:** The way external appearance affects cognitive confidence.\n\n### 2. Reading Text: The Power of First Impressions\nResearch shows that within the first seven seconds of meeting someone, people make subconscious judgments about character, competence, and reliability. In a famous experiment, doctors wearing clean white coats demonstrated higher diagnostic focus.\n\n### 3. Grammar Workshop: Articles (a, an, the, zero article)\n- Use a / an for non-specific singular countable nouns.\n- Use the when the listener knows which specific thing is meant.\n- Use zero article with plural or uncountable nouns spoken in general terms.";
}
if (_isPhysics) {
return "## الوحدة الأولى: المتجهات والكميات الفيزيائية\n\n### 1. التمييز بين الكميات القياسية والمتجهة\n- **الكميات القياسية:** تُحدد بالمقدار ووحدة القياس فقط (الكتلة، الزمن، الطاقة، درجة الحرارة).\n- **الكميات المتجهة:** تُحدد بالمقدار والاتجاه ونقطة التأثير (القوة، السرعة المتجهة، التسارع، الإزاحة).\n\n### 2. جمع وتحليل المتجهات بيانياً وتحليلياً\n- **المركبة الأفقية:** A_x = A cos θ\n- **المركبة الرأسية:** A_y = A sin θ\n- **المحصلة:** R = √(R_x² + R_y²)\n- **زاوية الاتجاه:** tan θ = |R_y / R_x|";
}
return "## الوحدة الأولى: المنهاج الوزاري المعتمد\n\n### 1. النتاجات والمفاهيم الأساسية\n- استيعاب المفاهيم والمصطلحات المعتمدة في الإطار العام للمناهج.\n- الربط بين المعرفة النظرية والتطبيقات العملية والتمارين الوزارية.\n- التحقق الذاتي من الاستيعاب عبر بنك الأسئلة والمختبر الذكي.";
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -162,6 +219,17 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
),
centerTitle: true,
actions: [
// Offline Save / Download Button
IconButton(
icon: Icon(
_isSavedLocally ? CupertinoIcons.checkmark_seal_fill : CupertinoIcons.cloud_download,
color: _isSavedLocally ? AppColors.emeraldGreen : Colors.white70,
size: 20,
),
tooltip: _isSavedLocally ? 'محفوظ في الجهاز (دون إنترنت)' : 'حفظ في الجهاز للقراءة دون إنترنت',
onPressed: _toggleSaveLocal,
),
// Font Resizing
IconButton(
icon: const Icon(CupertinoIcons.textformat_size, color: Colors.white70, size: 20),
onPressed: () {
@@ -171,6 +239,7 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
SaqelToast.showInfo(context, 'تم ضبط حجم الخط (${_fontSize.toInt()}pt)', title: 'حجم الخط');
},
),
// Share
IconButton(
icon: const Icon(CupertinoIcons.share, color: Colors.white70, size: 20),
onPressed: () {
@@ -258,36 +327,61 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
const SizedBox(height: 16),
],
// Document Header Status Banner
// Document Header Status Banner with Offline Status
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.saqelCyan.withAlpha(25),
_isSavedLocally ? AppColors.emeraldGreen.withAlpha(30) : AppColors.saqelCyan.withAlpha(25),
AppColors.appleBlue.withAlpha(20),
],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.saqelCyan.withAlpha(40)),
border: Border.all(
color: _isSavedLocally ? AppColors.emeraldGreen.withAlpha(60) : AppColors.saqelCyan.withAlpha(40),
),
),
child: Row(
children: [
const Icon(CupertinoIcons.doc_checkmark_fill, color: AppColors.saqelCyan, size: 22),
Icon(
_isSavedLocally ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.doc_checkmark_fill,
color: _isSavedLocally ? AppColors.emeraldGreen : AppColors.saqelCyan,
size: 22,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.documentType == 'textbook'
? 'الكتاب المدرسي المعتمد — وزارة التربية والتعليم'
: 'مذكرة دراسية وملخص معتمد',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
Row(
children: [
Text(
widget.documentType == 'textbook'
? 'الكتاب المدرسي المعتمد — وزارة التربية والتعليم'
: 'ورقة عمل ومذكرة دراسية معتمدة',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
),
const Spacer(),
if (_isSavedLocally)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withAlpha(30),
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'محفوظ في الجهاز 💾',
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 10, fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 2),
const SizedBox(height: 3),
Text(
'محتوى حي ومحدث مطابق لنتاجات ${widget.subjectTitle}',
_isSavedLocally
? 'مخزن في ذاكرة الجهاز — متاح للقراءة في أي وقت بدون إنترنت'
: 'محتوى حي ومحدث مطابق لنتاجات ${widget.subjectTitle}',
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
],
@@ -27,6 +27,8 @@ import 'islamic_interactive_lab_view.dart';
import 'history_interactive_timeline_view.dart';
import '../../../data/models/exam_model.dart';
import '../../../data/repositories/app_repositories.dart';
import '../../../core/services/local_document_storage_service.dart';
import '../../../data/datasources/curriculum_baked_data.dart';
/// الشاشة المركزية للمادة الدراسية وبوابات الدروس والامتحانات والمصادر
class SubjectHubScreen extends StatefulWidget {
@@ -292,13 +294,35 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
/// Tab 2: Worksheets & Summaries
Widget _buildWorksheetsTab(BuildContext context) {
final worksheets = widget.subject.worksheets.isNotEmpty
? widget.subject.worksheets
: [
const ResourceItemModel(title: 'ورقة عمل 1: المفاهيم الأساسية والتطبيقات', filePath: 'ws1.pdf', type: 'worksheet'),
const ResourceItemModel(title: 'ملخص شامل: القوانين والمعادلات الوزارية المقررة', filePath: 'summary.pdf', type: 'summary'),
const ResourceItemModel(title: 'مراجعة ختامية ونماذج تدريبية شاملة', filePath: 'exam_prep.pdf', type: 'worksheet'),
];
final s = widget.subject.id.toLowerCase();
final isMath = s.contains('math') || widget.subject.title.contains('رياضيات');
final isPhys = s.contains('physic') || widget.subject.title.contains('فيزياء');
final isEng = s.contains('english') || widget.subject.title.contains('إنجليز');
final defaultWorksheets = isMath
? [
const ResourceItemModel(title: 'ورقة عمل 1: الأسس والأنظمة والمعادلات الخاصة', filePath: 'grade_10/math_10/semester_1/resources/worksheet_1.md', type: 'worksheet'),
const ResourceItemModel(title: 'ملخص شامل: قوانين المعادلات والتحليل إلى العوامل', filePath: 'math_summary.md', type: 'summary'),
const ResourceItemModel(title: 'مراجعة تدريبية: حل أنظمة المعادلات بيانياً وجبرياً', filePath: 'math_exam_prep.md', type: 'worksheet'),
]
: (isPhys
? [
const ResourceItemModel(title: 'ورقة عمل وتطبيقات: تحليل المتجهات وقوانين نيوتن', filePath: 'physics_ws1.md', type: 'worksheet'),
const ResourceItemModel(title: 'ملخص شامل: الكميات القياسية والمتجهة والضرب النقطي', filePath: 'physics_summary.md', type: 'summary'),
const ResourceItemModel(title: 'دليل التجارب المخبرية: طاولة القوى والتسارع', filePath: 'physics_lab_guide.md', type: 'worksheet'),
]
: (isEng
? [
const ResourceItemModel(title: 'Action Pack 10 — Practice Worksheet: Unit 01 (Looking Good)', filePath: 'english_ws1.md', type: 'worksheet'),
const ResourceItemModel(title: 'Grammar & Vocabulary Revision: Articles & First Impressions', filePath: 'english_summary.md', type: 'summary'),
const ResourceItemModel(title: 'Unit 02 Reading & Grammar Worksheet (The Digital Mind)', filePath: 'english_ws2.md', type: 'worksheet'),
]
: [
const ResourceItemModel(title: 'ورقة عمل 1: المفاهيم الأساسية والتطبيقات', filePath: 'ws1.md', type: 'worksheet'),
const ResourceItemModel(title: 'ملخص شامل: القوانين والمعادلات الوزارية المقررة', filePath: 'summary.md', type: 'summary'),
]));
final worksheets = widget.subject.worksheets.isNotEmpty ? widget.subject.worksheets : defaultWorksheets;
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
@@ -476,12 +500,32 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
/// Tab 4: Official Ministry Textbooks
Widget _buildTextbooksTab(BuildContext context) {
final textbooks = widget.subject.textbooks.isNotEmpty
? widget.subject.textbooks
: [
const ResourceItemModel(title: 'كتاب الطالب المقرّر — منهاج وزارة التربية والتعليم', filePath: 'book.pdf', type: 'textbook'),
const ResourceItemModel(title: 'كتاب التجارب والأنشطة العلمية والعملية', filePath: 'workbook.pdf', type: 'textbook'),
];
final s = widget.subject.id.toLowerCase();
final isMath = s.contains('math') || widget.subject.title.contains('رياضيات');
final isPhys = s.contains('physic') || widget.subject.title.contains('فيزياء');
final isEng = s.contains('english') || widget.subject.title.contains('إنجليز');
final defaultTextbooks = isMath
? [
const ResourceItemModel(title: 'كتاب الطالب المقرر — الرياضيات (أنظمة المعادلات والدائرة)', filePath: 'grade_10/math_10/semester_1/math_student_book.md', type: 'textbook'),
const ResourceItemModel(title: 'كتاب التمارين والأنشطة الإضافية — الرياضيات 10', filePath: 'grade_10/math_10/semester_1/math_workbook.md', type: 'textbook'),
]
: (isPhys
? [
const ResourceItemModel(title: 'كتاب الفيزياء المقرر — الطالب (المتجهات والحركة)', filePath: 'grade_10/physics_10/semester_1/physics_student_book.md', type: 'textbook'),
const ResourceItemModel(title: 'كتاب التجارب والأنشطة العلمية والعملية (طاولة القوى)', filePath: 'grade_10/physics_10/semester_1/physics_activities_book.md', type: 'textbook'),
]
: (isEng
? [
const ResourceItemModel(title: 'Action Pack 10 — Student\'s Book (Looking Good & The Digital Mind)', filePath: 'grade_10/english_10/semester_1/unit_01.md', type: 'textbook'),
const ResourceItemModel(title: 'Action Pack 10 — Activity Book & Literature Spot', filePath: 'grade_10/english_10/semester_1/activity_book.md', type: 'textbook'),
]
: [
const ResourceItemModel(title: 'كتاب الطالب المقرّر — منهاج وزارة التربية والتعليم', filePath: 'book.md', type: 'textbook'),
const ResourceItemModel(title: 'كتاب التجارب والأنشطة العلمية والعملية', filePath: 'workbook.md', type: 'textbook'),
]));
final textbooks = widget.subject.textbooks.isNotEmpty ? widget.subject.textbooks : defaultTextbooks;
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
@@ -513,7 +557,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
),
const SizedBox(height: 4),
const Text(
'منهاج وزارة التربية والتعليم الأردنية المعتمد (PDF كامل)',
'منهاج وزارة التربية والتعليم الأردنية المعتمد (جاهز للقراءة والتنزيل)',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
],
@@ -592,9 +636,29 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
onPressed: () async {
Navigator.of(ctx).pop();
SaqelToast.showSuccess(context, 'تم تفعيل وضع القراءة دون إنترنت 📖', title: 'حفظ محلي');
final docType = title.contains('كتاب') ? 'textbook' : 'worksheet';
final content = CurriculumBakedData.getDocument(
subjectId: widget.subject.id,
type: docType,
filePath: filePath,
title: title,
);
await LocalDocumentStorageService.saveDocumentLocally(
subjectId: widget.subject.id,
type: docType,
filePath: filePath ?? title,
title: title,
content: content,
);
if (context.mounted) {
SaqelToast.showSuccess(
context,
'تم حفظ المستند في الجهاز — متاح الآن للقراءة دون إنترنت 📖',
title: 'حفظ محلي مكتمل',
);
}
},
icon: const Icon(CupertinoIcons.cloud_download, size: 18),
label: const Text('حفظ في الجهاز', style: TextStyle(fontWeight: FontWeight.w700)),
+6
View File
@@ -3,6 +3,8 @@ PODS:
- FlutterMacOS
- flutter_secure_storage_macos (6.1.3):
- FlutterMacOS
- flutter_tts (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- shared_preferences_foundation (0.0.1):
- Flutter
@@ -14,6 +16,7 @@ PODS:
DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- flutter_tts (from `Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
@@ -23,6 +26,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
flutter_secure_storage_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
flutter_tts:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos
FlutterMacOS:
:path: Flutter/ephemeral
shared_preferences_foundation:
@@ -33,6 +38,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
flutter_tts: ae915565cc6948444b513acc8ee021993281e027
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52