Implement multi-teacher video selector, in-app curriculum document reader, 15-question Unit 1 exam bank, and interactive science simulation engine

This commit is contained in:
Hamza-Ayed
2026-09-03 15:16:46 +03:00
parent 7e0aae023a
commit c9c03892d1
7 changed files with 1219 additions and 40 deletions
@@ -0,0 +1,308 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../widgets/luxury_widgets.dart';
class CurriculumDocumentViewerScreen extends StatefulWidget {
final String title;
final String documentType; // 'worksheet', 'summary', 'textbook'
final String subjectTitle;
final String? customContent;
const CurriculumDocumentViewerScreen({
super.key,
required this.title,
required this.documentType,
required this.subjectTitle,
this.customContent,
});
@override
State<CurriculumDocumentViewerScreen> createState() => _CurriculumDocumentViewerScreenState();
}
class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewerScreen> {
double _fontSize = 14.5;
bool _showAnswer = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.darkBackground,
appBar: AppBar(
backgroundColor: AppColors.darkSurface,
elevation: 0,
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
widget.title,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
'${widget.subjectTitle} • تصفح وقراءة ذكية',
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w600),
),
],
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(CupertinoIcons.textformat_size, color: Colors.white70, size: 20),
onPressed: () {
setState(() {
_fontSize = _fontSize >= 18.0 ? 13.0 : _fontSize + 1.5;
});
SaqelToast.showInfo(context, 'تم ضبط حجم الخط (${_fontSize.toInt()}pt)', title: 'حجم الخط');
},
),
IconButton(
icon: const Icon(CupertinoIcons.share, color: Colors.white70, size: 20),
onPressed: () {
SaqelToast.showSuccess(context, 'تم نسخ رابط المستند لمشاركته مع الزملاء', title: 'مشاركة الوثيقة 🔗');
},
),
],
),
body: Directionality(
textDirection: TextDirection.rtl,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Document Status Banner
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.saqelCyan.withAlpha(25),
AppColors.appleBlue.withAlpha(20),
],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.saqelCyan.withAlpha(40)),
),
child: Row(
children: [
const Icon(CupertinoIcons.doc_checkmark_fill, color: 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),
),
const SizedBox(height: 2),
const Text(
'تمت مراجعة المحتوى وفهرسته ومطابقته لنتاجات الفصل الدراسي الأول',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Section 1: Objectives & Learning Outcomes
_buildSectionCard(
title: 'الأهداف والنتاجات المستهدفة 🎯',
content: [
'1. حل المعادلات الخاصة بتحليل المقادير الجبرية وإخراج العامل المشترك الأكبر.',
'2. إيجاد نقاط التقاطع بين مستقيم وقطع مكافئ هندسياً وجبرياً بطريقة التعويض.',
'3. استخدام المميز (Δ) لتحديد عدد الحلول الحقيقية للنظام (حلان، حل وحيد، أو لا يوجد حل).',
'4. حل أنظمة المعادلات المكوّنة من معادلتيْن تربيعيتيْن بطريقة الحذف أو التعويض.',
'5. توظيف برمجية جيوجبرا في تمثيل المنحنيات وإيجاد نقط التقاطع بدقة.',
],
),
const SizedBox(height: 16),
// Section 2: Core Theorems and Golden Rules
_buildSectionCard(
title: 'القواعد الذهبية والقوانين الوزارية 💡',
content: [
'• قاعدة العامل الصفري: إذا كان A × B = 0 فإن A = 0 أو B = 0.',
'• الصورة التربيعية: المعادلة ax⁴ + bx² + c = 0 تُحل بالتعويض u = x² لتصبح au² + bu + c = 0.',
'• مميز المعادلة التربيعية: Δ = b² - 4ac:\n - إذا كان Δ > 0: للنظام حلّان حقيقيان مختلفان (يتقاطع المستقيم مع المنحنى في نقطتين).\n - إذا كان Δ = 0: للنظام حل حقيقي وحيد (المستقيم مماس للمنحنى).\n - إذا كان Δ < 0: لا يوجد أي حل حقيقي للنظام (المستقيم لا يتقاطع مع المنحنى).',
'• تقاطع دائرة وقطع مكافئ: أقصى عدد ممكن لنقاط التقاطع الحقيقية هو 4 نقاط.',
],
),
const SizedBox(height: 16),
// Section 3: Solved Examples Step-by-Step
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'مثال نموذجي محلول خطوة بخطوة ✍️',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 15),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withAlpha(30),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'سؤال وزاري متوقع',
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 11, fontWeight: FontWeight.w700),
),
),
],
),
const SizedBox(height: 12),
Text(
'المسألة: حُلّ نظام المعادلات التالي في مجموعة الأعداد الحقيقية:\n(1) y = x + 1\n(2) y = x² + 1',
style: TextStyle(
color: Colors.white,
fontSize: _fontSize,
fontWeight: FontWeight.w600,
height: 1.6,
),
),
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'خطوات الحل النموذجي:',
style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13),
),
const SizedBox(height: 8),
Text(
'1. بمساواة المعادلتين بالتعويض عن قيمة y:\n x² + 1 = x + 1\n\n'
'2. بطرح (x + 1) من الطرفين لجعل المعادلة صفرية:\n x² - x = 0\n\n'
'3. بإخراج x كعامل مشترك أكبر:\n x(x - 1) = 0\n\n'
'4. إما x = 0 أو x = 1.\n\n'
'5. إيجاد قيمة y بالتعويض في المعادلة الخطية:\n'
' - عندما x = 0 => y = 0 + 1 = 1 => نقطة التقاطع الأولى (0, 1)\n'
' - عندما x = 1 => y = 1 + 1 = 2 => نقطة التقاطع الثانية (1, 2)\n\n'
'مجموعة حل النظام هي: { (0, 1), (1, 2) }',
style: TextStyle(
color: AppColors.textSecondaryDark,
fontSize: _fontSize - 0.5,
height: 1.6,
fontFamily: 'SF Pro Text',
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Section 4: Self-Test Exercise with Reveal Answer
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(CupertinoIcons.pencil_ellipsis_rectangle, color: AppColors.guardianAmber, size: 20),
SizedBox(width: 8),
Text(
'تمرين تدريبي للتقييم الذاتي 📝',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 15),
),
],
),
const SizedBox(height: 10),
Text(
'حُلّ النظام التربيعي التالي بطريقة الحذف:\n(1) x² + y² = 25\n(2) x² - y² = 7',
style: TextStyle(color: Colors.white, fontSize: _fontSize, height: 1.6),
),
const SizedBox(height: 14),
Center(
child: TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: AppColors.saqelCyan,
backgroundColor: AppColors.saqelCyan.withAlpha(20),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
setState(() => _showAnswer = !_showAnswer);
},
icon: Icon(_showAnswer ? CupertinoIcons.eye_slash : CupertinoIcons.eye, size: 18),
label: Text(_showAnswer ? 'إخفاء الحل النموذجي' : 'إظهار الحل النموذجي للتحقق 🔍'),
),
),
if (_showAnswer) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withAlpha(15),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.emeraldGreen.withAlpha(50)),
),
child: Text(
'الحل:\nبجمع المعادلتين: 2x² = 32 => x² = 16 => x = ±4\n'
'بالتعويض في الأولى: 16 + y² = 25 => y² = 9 => y = ±3\n'
'حلول النظام الأربعة هي: (4, 3), (4, -3), (-4, 3), (-4, -3).',
style: TextStyle(color: Colors.white, fontSize: _fontSize - 1, height: 1.5),
),
),
],
],
),
),
const SizedBox(height: 30),
],
),
),
),
);
}
Widget _buildSectionCard({required String title, required List<String> content}) {
return LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 15),
),
const SizedBox(height: 12),
...content.map((line) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
line,
style: TextStyle(
color: AppColors.textSecondaryDark,
fontSize: _fontSize,
height: 1.55,
),
),
);
}),
],
),
);
}
}
@@ -7,6 +7,7 @@ import '../../../data/models/subject_model.dart';
import '../../widgets/luxury_widgets.dart';
import '../player/socratic_video_player_screen.dart';
import '../exams/adaptive_exam_screen.dart';
import 'curriculum_document_viewer_screen.dart';
class SubjectHubScreen extends StatefulWidget {
final SubjectModel subject;
@@ -184,14 +185,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
),
trailing: const Icon(CupertinoIcons.chevron_back, color: AppColors.textSecondaryDark, size: 16),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => SocraticVideoPlayerScreen(
lesson: lesson,
subject: widget.subject,
),
),
);
_showLessonVideoSelector(context, lesson);
},
),
);
@@ -265,9 +259,9 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
/// Tab 3: Question Bank & Adaptive Unit Exams
Widget _buildExamsTab(BuildContext context) {
final exams = [
const SubjectExamModel(id: 'ex1', title: 'اختبار الفهم الشامل: الوحدة الأولى', questionsCount: 25, durationMinutes: 45, targetScore: 100),
const SubjectExamModel(id: 'ex2', title: 'نماذج أسئلة الوزارة للسنوات السابقة', questionsCount: 30, durationMinutes: 60, targetScore: 100),
const SubjectExamModel(id: 'ex3', title: 'اختبار تشخيص الثغرات التكيفي بالذكاء الاصطناعي', questionsCount: 15, durationMinutes: 25, targetScore: 100),
const SubjectExamModel(id: 'ex1', title: 'اختبار الفهم الشامل: الوحدة الأولى (أنظمة المعادلات)', questionsCount: 15, durationMinutes: 45, targetScore: 100),
const SubjectExamModel(id: 'ex2', title: 'نماذج أسئلة الوزارة للسنوات السابقة', questionsCount: 15, durationMinutes: 45, targetScore: 100),
const SubjectExamModel(id: 'ex3', title: 'اختبار تشخيص الثغرات التكيفي بالذكاء الاصطناعي', questionsCount: 15, durationMinutes: 30, targetScore: 100),
];
return ListView.builder(
@@ -483,7 +477,15 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
),
onPressed: () {
Navigator.of(ctx).pop();
SaqelToast.showInfo(context, 'جاري عرض محتوى $title بالكامل عبر عارض الكتب الذكي', title: 'فتح الوثيقة 📑');
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => CurriculumDocumentViewerScreen(
title: title,
documentType: title.contains('كتاب') ? 'textbook' : 'worksheet',
subjectTitle: widget.subject.title,
),
),
);
},
icon: const Icon(CupertinoIcons.book, size: 18),
label: const Text('قراءة وتصفح', style: TextStyle(fontWeight: FontWeight.w800)),
@@ -497,4 +499,210 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
),
);
}
/// Multi-Teacher and AI Video Selector Bottom Sheet
void _showLessonVideoSelector(BuildContext context, CurriculumLessonItemModel lesson) {
showModalBottomSheet(
context: context,
backgroundColor: AppColors.darkSurface,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) => SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(25),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(CupertinoIcons.play_circle_fill, color: AppColors.saqelCyan, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'اختر أسلوب الشرح المناسب لك 🎥',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16),
),
const SizedBox(height: 3),
Text(
lesson.title,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
],
),
),
],
),
const SizedBox(height: 18),
// Option 1: Official Saqel AI Master Lesson
_buildVideoOptionCard(
title: 'شرح الذكاء الاصطناعي لمنصة صَقِل 🤖',
subtitle: 'الشرح الرسمي المعتمد وتفكيك المفاهيم خطوة بخطوة',
tag: 'المنهاج المعتمد + سقراطي',
tagColor: AppColors.saqelCyan,
icon: CupertinoIcons.sparkles,
iconColor: AppColors.saqelCyan,
onTap: () {
Navigator.of(ctx).pop();
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => SocraticVideoPlayerScreen(
lesson: lesson,
subject: widget.subject,
instructorName: 'منصة صَقِل الذكية 🤖',
videoSourceLabel: 'شرح الذكاء الاصطناعي الرسمي',
),
),
);
},
),
const SizedBox(height: 12),
// Option 2: Certified Teacher 1
_buildVideoOptionCard(
title: 'شرح الأستاذ رعد الخمايسة 👨‍🏫',
subtitle: 'خبير التوجيهي — حل نماذج وأسئلة وزارية إضافية',
tag: 'معلم معتمد 🌟 4.9',
tagColor: AppColors.guardianAmber,
icon: CupertinoIcons.person_crop_circle_badge_checkmark,
iconColor: AppColors.guardianAmber,
onTap: () {
Navigator.of(ctx).pop();
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => SocraticVideoPlayerScreen(
lesson: lesson,
subject: widget.subject,
instructorName: 'أ. رعد الخمايسة (معلم معتمد)',
videoSourceLabel: 'شرح الأستاذ رعد الخمايسة',
),
),
);
},
),
const SizedBox(height: 12),
// Option 3: Certified Teacher 2
_buildVideoOptionCard(
title: 'شرح الأستاذ إياد النجار 👨‍🏫',
subtitle: 'كبير المعلمين — تبسيط المفاهيم المعقدة والتحليل البياني',
tag: 'معلم معتمد 🌟 4.8',
tagColor: AppColors.emeraldGreen,
icon: CupertinoIcons.person_crop_circle_fill,
iconColor: AppColors.emeraldGreen,
onTap: () {
Navigator.of(ctx).pop();
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => SocraticVideoPlayerScreen(
lesson: lesson,
subject: widget.subject,
instructorName: 'أ. إياد النجار (كبير المعلمين)',
videoSourceLabel: 'شرح الأستاذ إياد النجار',
),
),
);
},
),
const SizedBox(height: 16),
],
),
),
),
);
}
Widget _buildVideoOptionCard({
required String title,
required String subtitle,
required String tag,
required Color tagColor,
required IconData icon,
required Color iconColor,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.darkCard,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: iconColor.withAlpha(25),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: iconColor, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13.5),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: tagColor.withAlpha(25),
borderRadius: BorderRadius.circular(6),
),
child: Text(
tag,
style: TextStyle(color: tagColor, fontSize: 10, fontWeight: FontWeight.w700),
),
),
],
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5),
),
],
),
),
const SizedBox(width: 8),
const Icon(CupertinoIcons.chevron_back, color: AppColors.textSecondaryDark, size: 14),
],
),
),
);
}
}
@@ -15,11 +15,15 @@ import 'package:video_player/video_player.dart';
class SocraticVideoPlayerScreen extends StatefulWidget {
final CurriculumLessonItemModel lesson;
final SubjectModel? subject;
final String? instructorName;
final String? videoSourceLabel;
const SocraticVideoPlayerScreen({
super.key,
required this.lesson,
this.subject,
this.instructorName,
this.videoSourceLabel,
});
@override
@@ -593,6 +597,28 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
),
],
),
if (widget.instructorName != null || widget.videoSourceLabel != null) ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(25),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(CupertinoIcons.person_crop_circle_fill, color: AppColors.saqelCyan, size: 14),
const SizedBox(width: 6),
Text(
widget.instructorName ?? widget.videoSourceLabel ?? 'شرح منصة صَقِل الذكية 🤖',
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
),
],
const SizedBox(height: 14),
if (widget.lesson.outcomes.isNotEmpty)
...widget.lesson.outcomes.map((out) {