Update Saqel Platform: 2026-09-12 13:38:49

This commit is contained in:
Hamza-Ayed
2026-09-12 13:38:49 +03:00
parent 1f31369893
commit f4d4f02853
34 changed files with 9136 additions and 386 deletions
@@ -0,0 +1,561 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../data/repositories/curriculum_question_bank.dart';
import '../../../data/repositories/error_notebook_repository.dart';
import '../../widgets/luxury_widgets.dart';
/// Interactive Lesson Homework Sheet
/// Enables students to practice questions derived from textbook lesson examples.
class LessonHomeworkSheet extends StatefulWidget {
final String subjectId;
final String subjectTitle;
final String lessonId;
final String lessonTitle;
const LessonHomeworkSheet({
super.key,
required this.subjectId,
required this.subjectTitle,
required this.lessonId,
required this.lessonTitle,
});
static Future<void> show(
BuildContext context, {
required String subjectId,
required String subjectTitle,
required String lessonId,
required String lessonTitle,
}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => FractionallySizedBox(
heightFactor: 0.92,
child: LessonHomeworkSheet(
subjectId: subjectId,
subjectTitle: subjectTitle,
lessonId: lessonId,
lessonTitle: lessonTitle,
),
),
);
}
@override
State<LessonHomeworkSheet> createState() => _LessonHomeworkSheetState();
}
class _LessonHomeworkSheetState extends State<LessonHomeworkSheet> {
late LessonHomeworkModel _homework;
int _currentIndex = 0;
int? _selectedOption;
bool _hasChecked = false;
int _correctCount = 0;
bool _isCompleted = false;
@override
void initState() {
super.initState();
_homework = CurriculumQuestionBank.getLessonHomework(
subjectId: widget.subjectId,
lessonId: widget.lessonId,
lessonTitle: widget.lessonTitle,
);
}
void _checkAnswer() {
if (_selectedOption == null || _hasChecked) return;
setState(() {
_hasChecked = true;
if (_selectedOption == _homework.questions[_currentIndex].correctIndex) {
_correctCount++;
} else {
_recordMistakeToNotebook(_homework.questions[_currentIndex], _selectedOption!);
}
});
}
void _recordMistakeToNotebook(LessonHomeworkItem q, int selectedOpt) {
final wrongAnswer = (selectedOpt >= 0 && selectedOpt < q.options.length)
? q.options[selectedOpt]
: 'إجابة غير صحيحة';
final correctAnswer = (q.correctIndex >= 0 && q.correctIndex < q.options.length)
? q.options[q.correctIndex]
: '';
ErrorNotebookRepository().logError(
subjectId: widget.subjectId,
subjectName: widget.subjectTitle,
topicName: widget.lessonTitle,
sourceType: 'adaptive_exam',
questionText: q.questionText,
studentWrongAnswer: wrongAnswer,
correctAnswer: correctAnswer,
hint: q.explanation,
errorCategory: 'conceptual',
options: q.options,
);
}
void _nextQuestion() {
if (_currentIndex + 1 < _homework.questions.length) {
setState(() {
_currentIndex++;
_selectedOption = null;
_hasChecked = false;
});
} else {
setState(() {
_isCompleted = true;
});
}
}
void _restart() {
setState(() {
_currentIndex = 0;
_selectedOption = null;
_hasChecked = false;
_correctCount = 0;
_isCompleted = false;
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: AppColors.darkBackground,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
border: Border(
top: BorderSide(color: AppColors.darkCardBorder, width: 1.5),
),
),
child: Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: [
// Sheet Handle
Center(
child: Container(
margin: const EdgeInsets.only(top: 10, bottom: 8),
width: 44,
height: 4.5,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(3),
),
),
),
// Header Bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.appleBlue.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(CupertinoIcons.pencil_ellipsis_rectangle,
color: AppColors.saqelCyan, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'واجب الدرس: ${widget.lessonTitle}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 15,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'${widget.subjectTitle} • تمارين تطبيقية على نمط المنهاج الوزاري',
style: const TextStyle(
color: AppColors.textSecondaryDark,
fontSize: 11.5,
),
),
],
),
),
IconButton(
icon: const Icon(CupertinoIcons.xmark_circle_fill,
color: Colors.white38, size: 24),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(color: AppColors.darkCardBorder, height: 1),
// Content Area
Expanded(
child: _isCompleted ? _buildCompletedView() : _buildQuestionView(),
),
],
),
),
);
}
Widget _buildQuestionView() {
if (_homework.questions.isEmpty) {
return const Center(
child: Text(
'لا توجد أسئلة واجب لهذا الدرس حالياً.',
style: TextStyle(color: AppColors.textSecondaryDark),
),
);
}
final q = _homework.questions[_currentIndex];
final total = _homework.questions.length;
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
children: [
// Progress Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'التمرين ${_currentIndex + 1} من $total',
style: const TextStyle(
color: AppColors.saqelCyan,
fontWeight: FontWeight.w700,
fontSize: 12.5,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${q.points} نقاط إتقان',
style: const TextStyle(
color: AppColors.emeraldGreen,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: 8),
// Linear Progress
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (_currentIndex + 1) / total,
backgroundColor: Colors.white10,
valueColor: const AlwaysStoppedAnimation(AppColors.saqelCyan),
minHeight: 5,
),
),
const SizedBox(height: 18),
// Question Card
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
q.questionText,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 15,
height: 1.5,
),
),
const SizedBox(height: 16),
// Options
...List.generate(q.options.length, (idx) {
final isSelected = _selectedOption == idx;
final isCorrect = idx == q.correctIndex;
Color borderColor = Colors.white12;
Color bgColor = const Color(0xFF09111E);
Widget? trailingIcon;
if (_hasChecked) {
if (isCorrect) {
borderColor = AppColors.emeraldGreen;
bgColor = AppColors.emeraldGreen.withValues(alpha: 0.15);
trailingIcon = const Icon(CupertinoIcons.checkmark_circle_fill,
color: AppColors.emeraldGreen, size: 20);
} else if (isSelected) {
borderColor = AppColors.crimsonRed;
bgColor = AppColors.crimsonRed.withValues(alpha: 0.15);
trailingIcon = const Icon(CupertinoIcons.xmark_circle_fill,
color: AppColors.crimsonRed, size: 20);
}
} else if (isSelected) {
borderColor = AppColors.saqelCyan;
bgColor = AppColors.saqelCyan.withValues(alpha: 0.12);
}
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: _hasChecked
? null
: () {
setState(() {
_selectedOption = idx;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor, width: isSelected || (_hasChecked && isCorrect) ? 1.5 : 1),
),
child: Row(
children: [
Container(
width: 26,
height: 26,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? AppColors.saqelCyan : Colors.white10,
),
child: Text(
String.fromCharCode(0x0623 + idx), // أ, ب, ج, د
style: TextStyle(
color: isSelected ? Colors.black : Colors.white,
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
q.options[idx],
style: TextStyle(
color: isSelected || (_hasChecked && isCorrect) ? Colors.white : Colors.white70,
fontSize: 13.5,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
),
if (trailingIcon != null) trailingIcon,
],
),
),
),
);
}),
],
),
),
// Explanation Card upon verification
if (_hasChecked) ...[
const SizedBox(height: 14),
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_selectedOption == q.correctIndex
? CupertinoIcons.check_mark_circled_solid
: CupertinoIcons.info_circle_fill,
color: _selectedOption == q.correctIndex
? AppColors.emeraldGreen
: AppColors.guardianAmber,
size: 18,
),
const SizedBox(width: 8),
Text(
_selectedOption == q.correctIndex
? 'إجابة صحيحة! خطوات التعليل والحل النموذجي:'
: 'الشرح التوضيحي والتعليل الشرعي / العلمي:',
style: TextStyle(
color: _selectedOption == q.correctIndex
? AppColors.emeraldGreen
: AppColors.guardianAmber,
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
],
),
const SizedBox(height: 8),
Text(
q.explanation,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
height: 1.6,
),
),
if (q.ruleTakeaway != null) ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
const Icon(CupertinoIcons.lightbulb_fill, color: AppColors.saqelCyan, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
'القاعدة الذهبية: ${q.ruleTakeaway!}',
style: const TextStyle(
color: AppColors.saqelCyan,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
],
],
),
),
],
const SizedBox(height: 20),
// Action Button
if (!_hasChecked)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _selectedOption != null ? AppColors.appleBlue : Colors.white12,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: _selectedOption != null ? _checkAnswer : null,
child: const Text(
'تحقق من الإجابة 🚀',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800),
),
)
else
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: _nextQuestion,
child: Text(
_currentIndex + 1 < total ? 'التمرين التالي ⬅️' : 'عرض النتيجة النهائية 🏆',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900),
),
),
],
);
}
Widget _buildCompletedView() {
final total = _homework.questions.length;
final percentage = total > 0 ? (_correctCount / total) * 100 : 0;
final isMastered = percentage >= 70;
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isMastered
? AppColors.emeraldGreen.withValues(alpha: 0.15)
: AppColors.guardianAmber.withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: Icon(
isMastered ? CupertinoIcons.rosette : CupertinoIcons.refresh_circled,
color: isMastered ? AppColors.emeraldGreen : AppColors.guardianAmber,
size: 54,
),
),
const SizedBox(height: 16),
Text(
isMastered ? 'أحسنت! أتممت واجب الدرس بنجاح 🌟' : 'اكتمل حل التمارين! يمكنك المحاولة مجدداً لترسيخ الفهم 📚',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 16.5,
),
),
const SizedBox(height: 8),
Text(
'أجبت عن $_correctCount من أصل $total أسئلة بشكل صحيح (${percentage.toStringAsFixed(0)}%)',
style: const TextStyle(
color: AppColors.textSecondaryDark,
fontSize: 13.5,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white24),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: _restart,
icon: const Icon(CupertinoIcons.refresh, size: 18),
label: const Text('إعادة الحل', style: TextStyle(fontWeight: FontWeight.w700)),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(CupertinoIcons.check_mark, size: 18),
label: const Text('تم الفهم', style: TextStyle(fontWeight: FontWeight.w800)),
),
),
],
),
],
),
),
);
}
}
@@ -48,6 +48,10 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
// Selected Textbook Exercise Index (0 to 5)
int _selectedExerciseIndex = 0;
// Scaffolded Problem Solving & 60-Second Thinking Pause
int _revealedStep = 1;
bool _thinkingPauseCompleted = false;
final List<Map<String, dynamic>> _textbookExercises = [
{
'title': 'نشاط معمل جيوجبرا (صفحة 16)',
@@ -900,7 +904,7 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
}
// ============================================================================
// TAB 3: STEP-BY-STEP ALGEBRAIC SOLVER & SOCRATIC DISCRIMINANT RADAR
// TAB 3: STEP-BY-STEP ALGEBRAIC SOLVER & PROGRESSIVE THINKING RADAR
// ============================================================================
Widget _buildStepByStepSolverView() {
final active = _textbookExercises[_selectedExerciseIndex];
@@ -910,6 +914,104 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. Thinking Pause Card (مهلة التفكير البناء - دقيقة واحدة)
if (!_thinkingPauseCompleted)
Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E293B), Color(0xFF0F172A)],
),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.guardianAmber, width: 1.5),
boxShadow: const [
BoxShadow(color: Colors.black45, blurRadius: 10, offset: Offset(0, 4)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.guardianAmber.withAlpha(30),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(CupertinoIcons.lightbulb_fill, color: AppColors.guardianAmber, size: 22),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'وقفة تفكير ذهني قبل الحل (Thinking Pause) ⏱️',
style: TextStyle(color: Colors.white, fontSize: 14.5, fontWeight: FontWeight.w800),
),
Text(
'امنح عقلك دقيقة لتحديد استراتيجية الحل قبل كشف الخطوات',
style: TextStyle(color: AppColors.guardianAmber, fontSize: 11, fontWeight: FontWeight.w600),
),
],
),
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Text(
'نظام المعادلات المراد حله:\n${active['system']}',
style: const TextStyle(
fontFamily: 'Courier',
color: AppColors.saqelCyan,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 12),
const Text(
'💡 أسئلة التوجيه الذهني:',
style: TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.w700),
),
const SizedBox(height: 4),
const Text(
'1. أي المعادلتين أسهل لعزل أحد المتغيرين وجعله موضوعاً للقانون؟\n2. إذا عوضت المعادلة الخطية في التربيعية، ما نوع المعادلة الناتجة؟',
style: TextStyle(color: Colors.white70, fontSize: 11.5, height: 1.5),
),
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
height: 44,
child: ElevatedButton.icon(
icon: const Icon(CupertinoIcons.play_arrow_solid, size: 16),
label: const Text(
'أنا جاهز، ابدأ بناء الحل خطوة بخطوة 🚀',
style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w800),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.guardianAmber,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => setState(() => _thinkingPauseCompleted = true),
),
),
],
),
),
// 2. Step-by-Step Problem Construction
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -918,73 +1020,136 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
children: [
const Icon(CupertinoIcons.wand_rays_inverse, color: AppColors.saqelCyan, size: 22),
const SizedBox(width: 10),
Text(
'خوارزمية الحل الجبري المنهجي — ${active['title']}',
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'بناء الحل الجبري المنهجي — ${active['title']}',
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w800),
),
Text(
'تم كشف $_revealedStep من 4 خطوات منهجية',
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11),
),
],
),
),
IconButton(
icon: const Icon(CupertinoIcons.arrow_counterclockwise, color: Colors.white60, size: 18),
tooltip: 'إعادة تمرين التفكير',
onPressed: () => setState(() {
_revealedStep = 1;
_thinkingPauseCompleted = false;
}),
),
],
),
const SizedBox(height: 14),
const Text(
'الخطوة 1: جعل أحد المتغيرين موضوعاً للقانون',
style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13),
),
const Divider(color: AppColors.darkCardBorder, height: 20),
// Step 1 (Always shown if unlocked)
_buildSolverStepHeader(1, 'جعل أحد المتغيرين موضوعاً للقانون', true),
const SizedBox(height: 4),
Text(
'من المعادلة الثانية: نجعل المتغير الخطي أو التربيعي في طرف مستقل:\n${active['eq2']}',
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 14),
const Text(
'الخطوة 2: التعويض في المعادلة الأولى',
style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13),
),
const SizedBox(height: 4),
Text(
'نعوض التعبير الجبري في معادلة المنحنى الأول: ${active['eq1']} للحصول على معادلة بمتغير واحد.',
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 14),
const Text(
'الخطوة 3: حساب المميز الجنائي (Discriminant: Δ = b² - 4ac)',
style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 13),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.guardianAmber.withAlpha(60)),
// Step 2
if (_revealedStep >= 2) ...[
_buildSolverStepHeader(2, 'التعويض في المعادلة الأولى وتصفير المعادلة', true),
const SizedBox(height: 4),
Text(
'نعوض التعبير الجبري في معادلة المنحنى الأول: ${active['eq1']} للحصول على معادلة بمتغير واحد.',
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
child: Row(
children: [
Text(
'قيمة المميز: Δ = ${active['delta']}',
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 14),
const SizedBox(height: 14),
],
// Step 3
if (_revealedStep >= 3) ...[
_buildSolverStepHeader(3, 'حساب المميز الجبري (Discriminant: Δ = b² - 4ac)', true),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.guardianAmber.withAlpha(60)),
),
child: Row(
children: [
Text(
'قيمة المميز: Δ = ${active['delta']}',
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 13),
),
const Spacer(),
Text(
(active['delta'] as double) > 0
? 'يوجد حلان حقيقيان (قاطع)'
: (active['delta'] as double) == 0
? 'يوجد حل حقيقي وحيد (مماس)'
: 'المميز سالب: لا يوجد تقاطع (∅)',
style: const TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold),
),
],
),
),
const SizedBox(height: 14),
],
// Step 4
if (_revealedStep >= 4) ...[
_buildSolverStepHeader(4, 'التحليل واستخراج نقاط التقاطع والتحقق', true),
const SizedBox(height: 4),
Text(
active['explanation'] as String,
style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.6),
),
const SizedBox(height: 14),
],
// Progression Action Button
if (_revealedStep < 4)
SizedBox(
width: double.infinity,
height: 42,
child: OutlinedButton.icon(
icon: const Icon(CupertinoIcons.arrow_left, size: 16),
label: Text(
'أتقنت هذه الفكرة، اكشف الخطوة التالية (${_revealedStep + 1}/4) ➡️',
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
const Spacer(),
Text(
(active['delta'] as double) > 0
? 'يوجد حلان حقيقيان (قاطع)'
: (active['delta'] as double) == 0
? 'يوجد حل حقيقي وحيد (مماس)'
: 'المميز سالب: لا يوجد تقاطع (∅)',
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.saqelCyan,
side: const BorderSide(color: AppColors.saqelCyan),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
],
onPressed: () => setState(() => _revealedStep++),
),
)
else
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withAlpha(20),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF10B981)),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.checkmark_seal_fill, color: Color(0xFF10B981), size: 18),
SizedBox(width: 8),
Text(
'🎉 أحسنت! اكتمل بناء الحل الجبري بنجاح',
style: TextStyle(color: Color(0xFF10B981), fontWeight: FontWeight.w800, fontSize: 12.5),
),
],
),
),
),
const SizedBox(height: 14),
const Text(
'الخطوة 4: التحليل واستخراج نقاط التقاطع النهائية',
style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.w700, fontSize: 13),
),
const SizedBox(height: 4),
Text(
active['explanation'] as String,
style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.6),
),
],
),
),
@@ -992,6 +1157,37 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
),
);
}
Widget _buildSolverStepHeader(int stepNumber, String title, bool isUnlocked) {
return Row(
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: isUnlocked ? AppColors.saqelCyan : Colors.white12,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'$stepNumber',
style: const TextStyle(color: Colors.black, fontSize: 11, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'الخطوة $stepNumber: $title',
style: TextStyle(
color: isUnlocked ? AppColors.saqelCyan : Colors.white60,
fontWeight: FontWeight.w700,
fontSize: 12.5,
),
),
),
],
);
}
}
// ==============================================================================
@@ -35,7 +35,10 @@ import '../virtual_labs/labs_registry.dart';
import '../virtual_labs/subject_virtual_labs_view.dart';
import '../../../data/models/exam_model.dart';
import '../../../data/repositories/app_repositories.dart';
import '../../../data/repositories/curriculum_question_bank.dart';
import '../../../data/repositories/curriculum_repository.dart';
import 'lesson_homework_sheet.dart';
import 'teacher_selection_sheet.dart';
/// الشاشة المركزية للمادة الدراسية وبوابات الدروس والامتحانات والمصادر
class SubjectHubScreen extends StatefulWidget {
@@ -95,10 +98,8 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
bool get _isHistorySubject =>
widget.subject.id.contains('hist') ||
widget.subject.title.contains('تاريخ') ||
widget.subject.title.contains('أردن') ||
widget.subject.title.contains('جغرافيا') ||
widget.subject.title.contains('دراسات');
(widget.subject.title.contains('تاريخ') && !widget.subject.title.contains('جغرافيا')) ||
(widget.subject.title.contains('أردن') && !widget.subject.title.contains('جغرافيا'));
bool get _hasLab =>
Grade10LabsRegistry.bySubjectNormalized(widget.subject.title).isNotEmpty ||
@@ -217,7 +218,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
void initState() {
super.initState();
_tabController = TabController(length: _hasLab ? 5 : 4, vsync: this);
_examsFuture = ExamRepository().getExams(courseId: 1, scope: 'unit_exam');
_examsFuture = ExamRepository().getExams(subjectCode: widget.subject.id, scope: 'unit_exam');
}
@override
@@ -243,15 +244,20 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
),
actions: [
IconButton(
tooltip: 'المختبرات الافتراضية لكل درس',
tooltip: 'المختبرات الافتراضية لمبحث ${widget.subject.title}',
icon: const Icon(CupertinoIcons.lab_flask_solid,
color: AppColors.saqelCyan, size: 22),
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => const Scaffold(
builder: (_) => Scaffold(
backgroundColor: AppColors.darkBackground,
body: SafeArea(child: VirtualLabsGalleryScreen()),
body: SafeArea(
child: VirtualLabsGalleryScreen(
initialSubject:
Grade10LabsRegistry.normalizeSubject(widget.subject.title),
),
),
),
),
);
@@ -438,7 +444,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
),
if (lab != null)
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => Grade10LabsRegistry.openLab(context, lab),
@@ -490,7 +496,121 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
),
),
),
)
else if (_hasLab)
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
if (_tabController.length > 1) {
_tabController.animateTo(1);
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
decoration: BoxDecoration(
color: widget.subject.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: widget.subject.primaryColor.withValues(alpha: 0.25)),
),
child: Row(
children: [
Icon(CupertinoIcons.lab_flask_solid, color: widget.subject.primaryColor, size: 14),
const SizedBox(width: 6),
Expanded(
child: Text(
'مختبر ${widget.subject.title} التفاعلي المعتمد',
style: const TextStyle(
color: Colors.white,
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: widget.subject.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'انتقل للمختبر',
style: TextStyle(
color: widget.subject.primaryColor,
fontSize: 10.5,
fontWeight: FontWeight.w800,
),
),
const SizedBox(width: 3),
Icon(CupertinoIcons.arrow_left, color: widget.subject.primaryColor, size: 10),
],
),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
LessonHomeworkSheet.show(
context,
subjectId: widget.subject.id,
subjectTitle: widget.subject.title,
lessonId: lesson.id,
lessonTitle: lesson.title,
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
decoration: BoxDecoration(
color: AppColors.appleBlue.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.appleBlue.withValues(alpha: 0.25)),
),
child: const Row(
children: [
Icon(CupertinoIcons.pencil_ellipsis_rectangle, color: AppColors.appleBlue, size: 14),
SizedBox(width: 6),
Expanded(
child: Text(
'واجب الدرس وتمارين الكتاب 📝',
style: TextStyle(
color: Colors.white,
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'حل التمارين',
style: TextStyle(
color: AppColors.saqelCyan,
fontSize: 10.5,
fontWeight: FontWeight.w800,
),
),
SizedBox(width: 3),
Icon(CupertinoIcons.arrow_left, color: AppColors.saqelCyan, size: 10),
],
),
],
),
),
),
),
],
),
);
@@ -503,7 +623,13 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
/// Tab 2: Worksheets & Summaries
Widget _buildWorksheetsTab(BuildContext context) {
final worksheets = widget.subject.worksheets;
final serverWorksheets = widget.subject.worksheets;
final worksheets = serverWorksheets.isNotEmpty
? serverWorksheets
: CurriculumQuestionBank.getSubjectWorksheets(
subjectId: widget.subject.id,
subjectTitle: widget.subject.title,
);
if (worksheets.isEmpty) {
return _buildUnavailableResourcesState(
@@ -596,13 +722,14 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
title: exam.title,
questionsCount: exam.questionsCount > 0 ? exam.questionsCount : 25,
durationMinutes: exam.durationMinutes > 0 ? exam.durationMinutes : 40,
subjectCode: widget.subject.id,
),
);
},
);
}
// If no server exams, generate unit exams from widget.subject.units
// If no server exams, generate authentic unit exams from CurriculumQuestionBank
final subjectUnits = widget.subject.units;
if (subjectUnits.isNotEmpty) {
return ListView.builder(
@@ -610,29 +737,47 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
itemCount: subjectUnits.length,
itemBuilder: (context, idx) {
final unit = subjectUnits[idx];
final fallbackExam = CurriculumQuestionBank.getUnitExam(
subjectId: widget.subject.id,
unitKey: unit.id,
unitTitle: unit.name,
subjectTitle: widget.subject.title,
);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildExamCard(
context,
examId: idx + 1,
title: 'اختبار الفهم التكيفي: ${unit.name}',
questionsCount: 25,
durationMinutes: 40,
examId: fallbackExam.id,
title: fallbackExam.title,
questionsCount: fallbackExam.questions.length,
durationMinutes: fallbackExam.durationMinutes,
subjectCode: widget.subject.id,
unitKey: unit.id,
initialExam: fallbackExam,
),
);
},
);
}
final defaultExam = CurriculumQuestionBank.getUnitExam(
subjectId: widget.subject.id,
unitKey: 'unit_01',
subjectTitle: widget.subject.title,
);
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
children: [
_buildExamCard(
context,
examId: 1,
title: 'اختبار الفهم الشامل: ${widget.subject.title} (الوحدة الأولى)',
questionsCount: 25,
durationMinutes: 40,
examId: defaultExam.id,
title: defaultExam.title,
questionsCount: defaultExam.questions.length,
durationMinutes: defaultExam.durationMinutes,
subjectCode: widget.subject.id,
unitKey: 'unit_01',
initialExam: defaultExam,
),
],
);
@@ -646,6 +791,9 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
required String title,
required int questionsCount,
required int durationMinutes,
String? subjectCode,
String? unitKey,
ExamModel? initialExam,
}) {
return LuxuryCard(
child: Column(
@@ -702,6 +850,9 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
examId: examId,
title: title,
subjectTitle: widget.subject.title,
subjectCode: subjectCode ?? widget.subject.id,
unitKey: unitKey,
initialExam: initialExam,
),
),
);
@@ -717,7 +868,13 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
/// Tab 4: Official Ministry Textbooks
Widget _buildTextbooksTab(BuildContext context) {
final textbooks = widget.subject.textbooks;
final serverTextbooks = widget.subject.textbooks;
final textbooks = serverTextbooks.isNotEmpty
? serverTextbooks
: CurriculumQuestionBank.getSubjectTextbooks(
subjectId: widget.subject.id,
subjectTitle: widget.subject.title,
);
if (textbooks.isEmpty) {
return _buildUnavailableResourcesState(
@@ -1065,17 +1222,10 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
if (videos.length == 1) {
chosen = videos.first;
} else {
chosen = await showCupertinoModalPopup<PublishedLessonVideoModel>(
context: context,
builder: (sheetContext) => CupertinoActionSheet(
title: const Text('اختر شرح المعلم'),
message: const Text('تُعرض الحصص المنشورة لهذا الدرس فقط، مرتبة بالتقييم الموثق.'),
actions: videos.map((video) => CupertinoActionSheetAction(
onPressed: () => Navigator.of(sheetContext).pop(video),
child: Text(video.ratingCount == 0 ? '${video.teacherName} — جديد' : '${video.teacherName} — ★ ${video.rating.toStringAsFixed(1)} (${video.ratingCount})'),
)).toList(),
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(sheetContext).pop(), child: const Text('إلغاء')),
),
chosen = await TeacherSelectionSheet.show(
context,
lesson: lesson,
videos: videos,
);
}
if (chosen == null || !context.mounted) return;
@@ -0,0 +1,393 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../data/models/subject_model.dart';
import '../../../data/repositories/curriculum_repository.dart';
import '../../widgets/luxury_widgets.dart';
/// Apple Cupertino Sheet for choosing a teacher when multiple published video versions exist for a lesson.
/// Conforms to Saqel product rule: "أكثر من حصة متاحة: قائمة المعلمين مرتبة بالتقييم الموثوق، مع عدد التقييمات والتصفح على دفعات."
class TeacherSelectionSheet extends StatelessWidget {
final CurriculumLessonItemModel lesson;
final List<PublishedLessonVideoModel> videos;
const TeacherSelectionSheet({
super.key,
required this.lesson,
required this.videos,
});
static Future<PublishedLessonVideoModel?> show(
BuildContext context, {
required CurriculumLessonItemModel lesson,
required List<PublishedLessonVideoModel> videos,
}) {
return showModalBottomSheet<PublishedLessonVideoModel>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) => TeacherSelectionSheet(
lesson: lesson,
videos: videos,
),
);
}
@override
Widget build(BuildContext context) {
// Sort server-authoritatively by rating desc, then rating count desc
final sortedVideos = List<PublishedLessonVideoModel>.from(videos)
..sort((a, b) {
final cmp = b.rating.compareTo(a.rating);
if (cmp != 0) return cmp;
return b.ratingCount.compareTo(a.ratingCount);
});
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.82,
),
decoration: const BoxDecoration(
color: AppColors.darkBackground,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
border: Border(
top: BorderSide(color: AppColors.darkCardBorder, width: 1.5),
),
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle
Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 44,
height: 5,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(3),
),
),
// Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(25),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
CupertinoIcons.person_2_fill,
color: AppColors.saqelCyan,
size: 20,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'اختر شرح المعلم المعتمد 👨‍🏫',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 2),
Text(
lesson.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textSecondaryDark,
fontSize: 12,
),
),
],
),
),
IconButton(
icon: const Icon(CupertinoIcons.xmark_circle_fill,
color: Colors.white38, size: 24),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(color: AppColors.darkCardBorder, height: 1),
// Explanatory badge
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(20),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.appleBlue.withAlpha(50)),
),
child: const Row(
children: [
Icon(CupertinoIcons.info_circle_fill,
color: AppColors.appleBlue, size: 16),
SizedBox(width: 10),
Expanded(
child: Text(
'لكل معلم أسلوب شرح مميز؛ الحصص مرتبة حسب تقييمات الطلاب الموثقة على المنصة.',
style: TextStyle(
color: AppColors.appleBlue,
fontSize: 11.5,
fontWeight: FontWeight.w600),
),
),
],
),
),
),
// Teacher Cards List
Flexible(
child: ListView.separated(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
shrinkWrap: true,
itemCount: sortedVideos.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (ctx, idx) {
final video = sortedVideos[idx];
final isNew = video.ratingCount == 0;
final initials = video.teacherName.isNotEmpty
? video.teacherName.trim().characters.first
: 'م';
final approaches = [
'تركيز على خطوات الحل الوزاري والتحليل المفاهيمي',
'تبسيط القواعد مع أمثلة حياتية وتطبيقات واقعية',
'تدريبات مكثفة ونماذج امتحانات وزارية سابقة',
'شرح تفاعلي مرئي مع استنتاج القوانين خطوة بخطوة',
];
final approach = approaches[idx % approaches.length];
return LuxuryCard(
borderColor: idx == 0
? AppColors.saqelCyan.withAlpha(90)
: AppColors.darkCardBorder,
child: InkWell(
onTap: () => Navigator.of(context).pop(video),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Avatar circle
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: idx == 0
? [
AppColors.appleBlue,
AppColors.saqelCyan
]
: [
AppColors.darkSurface,
AppColors.appleBlue.withAlpha(80)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(
color: idx == 0
? AppColors.saqelCyan
: Colors.white24,
width: 1.5,
),
),
child: Center(
child: Text(
initials,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(width: 14),
// Name & Verified
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
video.teacherName,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w800,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
const Icon(
CupertinoIcons.checkmark_seal_fill,
color: AppColors.saqelCyan,
size: 16,
),
if (idx == 0) ...[
const SizedBox(width: 6),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2),
decoration: BoxDecoration(
color: AppColors.guardianAmber
.withAlpha(30),
borderRadius:
BorderRadius.circular(6),
border: Border.all(
color: AppColors
.guardianAmber
.withAlpha(100)),
),
child: const Text(
'الأعلى تقييماً',
style: TextStyle(
color: AppColors.guardianAmber,
fontSize: 9.5,
fontWeight: FontWeight.w700,
),
),
),
],
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
CupertinoIcons.star_fill,
color: AppColors.guardianAmber,
size: 13,
),
const SizedBox(width: 4),
Text(
isNew
? 'حصة جديدة'
: '${video.rating.toStringAsFixed(1)} ★',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
if (!isNew) ...[
const SizedBox(width: 6),
Text(
'(${video.ratingCount} تقييم موثق)',
style: const TextStyle(
color:
AppColors.textSecondaryDark,
fontSize: 11,
),
),
],
const SizedBox(width: 10),
const Icon(
CupertinoIcons.checkmark_shield_fill,
color: AppColors.emeraldGreen,
size: 12,
),
const SizedBox(width: 4),
const Text(
'معتمد رسمياً',
style: TextStyle(
color: AppColors.textSecondaryDark,
fontSize: 11,
),
),
],
),
],
),
),
const Icon(
CupertinoIcons.chevron_left,
color: AppColors.saqelCyan,
size: 18,
),
],
),
const SizedBox(height: 10),
// Pedagogical approach badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: const Color(0xFF060B14),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
const Icon(
CupertinoIcons.lightbulb_fill,
color: AppColors.guardianAmber,
size: 12,
),
const SizedBox(width: 6),
Expanded(
child: Text(
approach,
style: const TextStyle(
color: AppColors.textSecondaryDark,
fontSize: 11,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
),
);
},
),
),
const SizedBox(height: 12),
],
),
),
);
}
}