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
@@ -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)),