Enhance video playback cubit resilience and K-12 curriculum flow
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
import '../player/socratic_video_player_screen.dart';
|
||||
|
||||
class SubjectHubScreen extends StatefulWidget {
|
||||
final SubjectModel subject;
|
||||
|
||||
const SubjectHubScreen({super.key, required this.subject});
|
||||
|
||||
@override
|
||||
State<SubjectHubScreen> createState() => _SubjectHubScreenState();
|
||||
}
|
||||
|
||||
class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(CupertinoIcons.back, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(
|
||||
widget.subject.title,
|
||||
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: widget.subject.primaryColor,
|
||||
indicatorWeight: 3,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: AppColors.textSecondaryDark,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
tabs: const [
|
||||
Tab(icon: Icon(CupertinoIcons.play_rectangle_fill, size: 18), text: 'الفيديوهات'),
|
||||
Tab(icon: Icon(CupertinoIcons.doc_text_fill, size: 18), text: 'أوراق العمل'),
|
||||
Tab(icon: Icon(CupertinoIcons.sparkles, size: 18), text: 'بنك الأسئلة'),
|
||||
Tab(icon: Icon(CupertinoIcons.book_fill, size: 18), text: 'الكتب المقررة'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildLessonsTab(context),
|
||||
_buildWorksheetsTab(context),
|
||||
_buildExamsTab(context),
|
||||
_buildTextbooksTab(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Tab 1: Interactive Video Lessons by Unit
|
||||
Widget _buildLessonsTab(BuildContext context) {
|
||||
final units = widget.subject.units;
|
||||
|
||||
if (units.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(CupertinoIcons.play_circle, size: 56, color: widget.subject.primaryColor.withAlpha(120)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'جاري تحضير دروس ${widget.subject.title} بواسطة الذكاء الاصطناعي',
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||
itemCount: units.length,
|
||||
itemBuilder: (context, uIdx) {
|
||||
final unit = units[uIdx];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: uIdx == 0,
|
||||
shape: const Border(),
|
||||
collapsedShape: const Border(),
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.subject.primaryColor.withAlpha(30),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${uIdx + 1}',
|
||||
style: TextStyle(
|
||||
color: widget.subject.primaryColor,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
unit.name,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 15),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${unit.lessons.length} دروس تفاعلية • فحص فهم سقراطي',
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
children: unit.lessons.map((lesson) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF09111E),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.darkCardBorder.withAlpha(80)),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
|
||||
leading: Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [widget.subject.primaryColor, widget.subject.secondaryColor],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(CupertinoIcons.play_fill, color: Colors.black, size: 18),
|
||||
),
|
||||
title: Text(
|
||||
lesson.title,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 13),
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${lesson.durationSeconds ~/ 60} دقيقة',
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${lesson.checkpointsCount} فحوصات',
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 10, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: const Icon(CupertinoIcons.chevron_back, color: AppColors.textSecondaryDark, size: 16),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => SocraticVideoPlayerScreen(
|
||||
lesson: lesson,
|
||||
subject: widget.subject,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 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'),
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||
itemCount: worksheets.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final ws = worksheets[idx];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: LuxuryCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.crimsonRed.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(CupertinoIcons.doc_fill, color: AppColors.crimsonRed, size: 22),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
ws.title,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'ملف PDF جاهز للطباعة والمراجعة السريعة',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.arrow_down_circle_fill, color: AppColors.saqelCyan, size: 28),
|
||||
onPressed: () {
|
||||
SaqelToast.showSuccess(context, 'تم تجهيز ملف ${ws.title} للتحميل', title: 'تحميل المذكرة 📄');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 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),
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||
itemCount: exams.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final exam = exams[idx];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
exam.title,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.emeraldGreen.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'جاهز للتقديم ✍️',
|
||||
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.question_circle, color: AppColors.textSecondaryDark, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text('${exam.questionsCount} سؤال اختياري', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
|
||||
const SizedBox(width: 14),
|
||||
const Icon(CupertinoIcons.time, color: AppColors.textSecondaryDark, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text('${exam.durationMinutes} دقيقة', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
|
||||
const Spacer(),
|
||||
LuxuryButton(
|
||||
text: 'بدء الاختبار 🚀',
|
||||
onPressed: () {
|
||||
SaqelToast.showInfo(context, 'جاري توليد أسئلة الاختبار التكيفي من بنك الوزارة', title: 'بدء الامتحان 📝');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 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'),
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||
itemCount: textbooks.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final tb = textbooks[idx];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: LuxuryCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 46,
|
||||
height: 46,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.appleBlue.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(CupertinoIcons.book_fill, color: AppColors.appleBlue, size: 22),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tb.title,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'منهاج وزارة التربية والتعليم الأردنية المعتمد (PDF كامل)',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.eye_fill, color: AppColors.saqelCyan, size: 24),
|
||||
onPressed: () {
|
||||
SaqelToast.showInfo(context, 'جاري فتح ${tb.title} عبر عارض الكتب الذكي', title: 'الكتاب الوزاري 📚');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../../data/repositories/curriculum_repository.dart';
|
||||
import '../../../logic/cubits/curriculum_cubit.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
import 'subject_hub_screen.dart';
|
||||
|
||||
class SubjectsGridScreen extends StatefulWidget {
|
||||
final String? initialGrade;
|
||||
const SubjectsGridScreen({super.key, this.initialGrade});
|
||||
|
||||
@override
|
||||
State<SubjectsGridScreen> createState() => _SubjectsGridScreenState();
|
||||
}
|
||||
|
||||
class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final grade = widget.initialGrade ?? 'grade_10';
|
||||
context.read<CurriculumCubit>().fetchSubjects(gradeLevel: grade);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
IconData _getCupertinoIcon(String iconCode) {
|
||||
switch (iconCode) {
|
||||
case 'calculator':
|
||||
return CupertinoIcons.number_square_fill;
|
||||
case 'book_closed_fill':
|
||||
return CupertinoIcons.book_fill;
|
||||
case 'chat_bubble_2_fill':
|
||||
return CupertinoIcons.chat_bubble_2_fill;
|
||||
case 'moon_stars_fill':
|
||||
return CupertinoIcons.moon_stars_fill;
|
||||
case 'bolt_fill':
|
||||
return CupertinoIcons.bolt_fill;
|
||||
case 'flame_fill':
|
||||
return CupertinoIcons.flame_fill;
|
||||
case 'tree_fill':
|
||||
return CupertinoIcons.tree;
|
||||
case 'globe':
|
||||
return CupertinoIcons.globe;
|
||||
case 'building_columns_fill':
|
||||
return CupertinoIcons.building_2_fill;
|
||||
case 'map_fill':
|
||||
return CupertinoIcons.map_fill;
|
||||
case 'person_2_fill':
|
||||
return CupertinoIcons.person_2_fill;
|
||||
case 'desktopcomputer':
|
||||
return CupertinoIcons.desktopcomputer;
|
||||
case 'creditcard_fill':
|
||||
return CupertinoIcons.money_dollar_circle_fill;
|
||||
case 'wrench_fill':
|
||||
return CupertinoIcons.wrench_fill;
|
||||
case 'paintbrush_fill':
|
||||
return CupertinoIcons.paintbrush_fill;
|
||||
case 'sportscourt_fill':
|
||||
return CupertinoIcons.sportscourt_fill;
|
||||
default:
|
||||
return CupertinoIcons.book_fill;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.appleBlue, AppColors.saqelCyan],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text('ص', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w900, fontSize: 18)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'المناهج والمباحث الدراسية',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 17),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.checkmark_seal_fill, color: AppColors.saqelCyan, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'المنهاج الوزاري الأردني 🇯🇴',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<CurriculumCubit, CurriculumState>(
|
||||
builder: (context, state) {
|
||||
if (state is CurriculumLoading) {
|
||||
return const Center(child: CircularProgressIndicator(color: AppColors.saqelCyan));
|
||||
}
|
||||
|
||||
if (state is CurriculumError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.wifi_exclamationmark, color: AppColors.crimsonRed, size: 48),
|
||||
const SizedBox(height: 14),
|
||||
Text(state.message, style: const TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: 14),
|
||||
LuxuryButton(
|
||||
text: 'إعادة المحاولة',
|
||||
onPressed: () => context.read<CurriculumCubit>().fetchSubjects(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is CurriculumLoaded) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Grade Level Header & Stage Switcher
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'المواد المقررة — ${GradeLevelModel.getGradeName(state.selectedGrade)} 📚',
|
||||
style: AppTypography.displayLarge.copyWith(color: Colors.white, fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'اختر المادة لتصفح شروحات الفيديو التفاعلية، المذكرات، وبنك الأسئلة',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: Text(
|
||||
'${state.filteredSubjects.length} مباحث متوفرة',
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// K-12 Grade Level Selector Bar (الصف الأول -> التوجيهي)
|
||||
SizedBox(
|
||||
height: 42,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: (state.availableGrades.isNotEmpty ? state.availableGrades : GradeLevelModel.allK12Grades).length,
|
||||
itemBuilder: (context, gIdx) {
|
||||
final grade = (state.availableGrades.isNotEmpty ? state.availableGrades : GradeLevelModel.allK12Grades)[gIdx];
|
||||
final isSelected = state.selectedGrade == grade.key || state.selectedGrade.replaceAll('grade_', '') == grade.key.replaceAll('grade_', '');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: InkWell(
|
||||
onTap: () => context.read<CurriculumCubit>().selectGrade(grade.key),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.saqelCyan.withAlpha(30) : AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.saqelCyan : AppColors.darkCardBorder,
|
||||
width: isSelected ? 1.5 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
grade.name,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.saqelCyan : AppColors.textSecondaryDark,
|
||||
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500,
|
||||
fontSize: 12.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Search Bar
|
||||
LuxuryTextField(
|
||||
controller: _searchController,
|
||||
hint: 'ابحث عن مادة دراسية (مثلاً: الفيزياء، الرياضيات، الإنجليزي)...',
|
||||
prefixIcon: const Icon(CupertinoIcons.search, color: AppColors.saqelCyan, size: 18),
|
||||
onChanged: (q) => context.read<CurriculumCubit>().searchSubjects(q),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Dynamic Live Subjects Bento Grid or Empty State
|
||||
if (state.filteredSubjects.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 60, horizontal: 20),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(20),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(CupertinoIcons.square_grid_2x2, size: 32, color: AppColors.saqelCyan),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'لا توجد مواد مضافة بعد في السيرفر',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'يتم إدراج المواد تلقائياً بمجرد رفع وتحليل كتب المناهج في الخادم',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
int crossAxisCount = 4;
|
||||
if (constraints.maxWidth < 600) {
|
||||
crossAxisCount = 2;
|
||||
} else if (constraints.maxWidth < 900) {
|
||||
crossAxisCount = 3;
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.15,
|
||||
),
|
||||
itemCount: state.filteredSubjects.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final sub = state.filteredSubjects[idx];
|
||||
return _buildSubjectCard(context, sub);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Individual Apple Bento Card for Subject
|
||||
Widget _buildSubjectCard(BuildContext context, SubjectModel subject) {
|
||||
final iconData = _getCupertinoIcon(subject.iconCode);
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => SubjectHubScreen(subject: subject),
|
||||
),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(color: AppColors.darkCardBorder, width: 1.2),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x33000000),
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
subject.primaryColor.withAlpha(50),
|
||||
subject.secondaryColor.withAlpha(20),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: subject.primaryColor.withAlpha(120), width: 1.5),
|
||||
),
|
||||
child: Icon(iconData, color: subject.primaryColor, size: 24),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(80),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${subject.totalUnits} وحدات',
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 10, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
subject.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 15,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subject.englishTitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondaryDark,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Mini Mastery Progress Bar
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: subject.masteryScore / 100.0,
|
||||
backgroundColor: Colors.white10,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(subject.primaryColor),
|
||||
minHeight: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../../../data/models/lesson_model.dart';
|
||||
import '../../../logic/cubits/auth_cubit.dart';
|
||||
import '../../../logic/cubits/dashboard_cubits.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
import '../curriculum/subjects_grid_screen.dart';
|
||||
|
||||
class UnifiedHomeScreen extends StatefulWidget {
|
||||
final UserModel user;
|
||||
@@ -209,13 +210,43 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
|
||||
);
|
||||
} else if (state is StudentEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.book, color: AppColors.textMutedDark, size: 48),
|
||||
const SizedBox(height: 12),
|
||||
Text(AppStrings.noLessonsFound, style: const TextStyle(color: AppColors.textSecondaryDark)),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(25),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80), width: 1.5),
|
||||
),
|
||||
child: const Icon(CupertinoIcons.book_fill, color: AppColors.saqelCyan, size: 36),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text(
|
||||
'المنهاج الوزاري المعتمد — صَقِل 🇯🇴',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'اختر من بين 16 مبحثاً دراسياً لبدء شروحات الفيديو التفاعلية، أوراق العمل، وبنك الأسئلة السقراطي',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13, height: 1.4),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
LuxuryButton(
|
||||
text: 'تصفح المناهج والمواد 📚',
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => SubjectsGridScreen(initialGrade: widget.user.gradeLevel)),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is StudentLoaded) {
|
||||
@@ -224,6 +255,69 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
// Subjects Catalog Banner (Top Gateway)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => SubjectsGridScreen(initialGrade: widget.user.gradeLevel)),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0F2B48), Color(0xFF0A192F)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(100), width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.appleBlue, AppColors.saqelCyan],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Icon(CupertinoIcons.square_grid_2x2_fill, color: Colors.black, size: 24),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'تصفح المناهج والمواد (16 مادة) 📚',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 15),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'الرياضيات، الفيزياء، الإنجليزي، الكيمياء، والمزيد',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(CupertinoIcons.chevron_back, color: AppColors.saqelCyan, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Real Readiness Score Card
|
||||
LuxuryCard(
|
||||
borderColor: AppColors.saqelCyan.withAlpha(50),
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../../logic/cubits/auth_cubit.dart';
|
||||
import '../../../logic/cubits/video_playback_cubit.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
import '../../widgets/socratic_dialog.dart';
|
||||
|
||||
class SocraticVideoPlayerScreen extends StatefulWidget {
|
||||
final CurriculumLessonItemModel lesson;
|
||||
final SubjectModel? subject;
|
||||
|
||||
const SocraticVideoPlayerScreen({
|
||||
super.key,
|
||||
required this.lesson,
|
||||
this.subject,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SocraticVideoPlayerScreen> createState() => _SocraticVideoPlayerScreenState();
|
||||
}
|
||||
|
||||
class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> with SingleTickerProviderStateMixin {
|
||||
Timer? _playbackTicker;
|
||||
late AnimationController _watermarkController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<VideoPlaybackCubit>().loadLesson(widget.lesson, subject: widget.subject);
|
||||
|
||||
// Dynamic Floating Forensic Anti-Piracy Watermark Animation
|
||||
_watermarkController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 18),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
// Ticker simulating video playback time progression and Socratic checkpoint gatekeeping
|
||||
_playbackTicker = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
final cubit = context.read<VideoPlaybackCubit>();
|
||||
final state = cubit.state;
|
||||
if (state is VideoPlaybackReady && state.isPlaying && state.activeCheckpoint == null) {
|
||||
if (state.currentPositionSeconds < state.playbackData.durationSeconds) {
|
||||
cubit.updatePosition(state.currentPositionSeconds + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_playbackTicker?.cancel();
|
||||
_watermarkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatTime(int seconds) {
|
||||
final m = seconds ~/ 60;
|
||||
final s = seconds % 60;
|
||||
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = context.read<AuthCubit>().state;
|
||||
final studentName = authState is Authenticated ? authState.user.name : 'طالب صَقِل';
|
||||
final studentPhone = authState is Authenticated ? authState.user.phone : '0798583052';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(CupertinoIcons.back, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(
|
||||
widget.lesson.title,
|
||||
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.shield_lefthalf_fill, color: AppColors.saqelCyan, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'حماية جنائية DRM',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocConsumer<VideoPlaybackCubit, VideoPlaybackState>(
|
||||
listener: (context, state) {
|
||||
if (state is VideoPlaybackReady) {
|
||||
// Trigger Socratic Checkpoint Modal automatically when active
|
||||
if (state.activeCheckpoint != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (modalContext) => SocraticCheckpointModal(
|
||||
checkpoint: state.activeCheckpoint!,
|
||||
onOptionSelected: (opt) {
|
||||
final isCorrect = context.read<VideoPlaybackCubit>().submitCheckpointAnswer(opt);
|
||||
if (isCorrect) {
|
||||
SaqelToast.showSuccess(context, 'إجابة نموذجية! تم تعزيز الجاهزية (+0.5%)', title: 'فحص الفهم السقراطي ✨');
|
||||
} else {
|
||||
SaqelToast.showError(context, 'تم إرجاع الفيديو لإعادة الاستماع للنقطة الجوهرية 🔄', title: 'سد الثغرة العلاجية');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is VideoPlaybackLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.saqelCyan),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is VideoPlaybackError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.exclamationmark_triangle_fill, color: AppColors.crimsonRed, size: 48),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message, style: const TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: 16),
|
||||
LuxuryButton(
|
||||
text: 'إعادة المحاولة',
|
||||
onPressed: () => context.read<VideoPlaybackCubit>().loadLesson(widget.lesson),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is VideoPlaybackReady) {
|
||||
final duration = state.playbackData.durationSeconds > 0 ? state.playbackData.durationSeconds : 960;
|
||||
final current = state.currentPositionSeconds;
|
||||
final progress = (current / duration).clamp(0.0, 1.0);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1000),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Video Viewport Canvas with Anti-Piracy Floating Watermark
|
||||
Container(
|
||||
height: 380,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x66000000),
|
||||
blurRadius: 30,
|
||||
offset: Offset(0, 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video Background Scene Simulation
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: Alignment(0.0, -0.2),
|
||||
radius: 1.2,
|
||||
colors: [
|
||||
Color(0xFF0F1E36),
|
||||
Color(0xFF050B14),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(25),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(100), width: 2),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
state.isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill,
|
||||
color: AppColors.saqelCyan,
|
||||
size: 36,
|
||||
),
|
||||
onPressed: () => context.read<VideoPlaybackCubit>().togglePlayPause(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.lesson.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'بث مشفر فائق الجودة HLS 1080p — منصة صَقِل',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondaryDark,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Dynamic Forensic Anti-Piracy Watermark (Moves continuously)
|
||||
AnimatedBuilder(
|
||||
animation: _watermarkController,
|
||||
builder: (context, child) {
|
||||
return Align(
|
||||
alignment: Alignment(
|
||||
-0.8 + (1.6 * _watermarkController.value),
|
||||
-0.7 + (1.4 * _watermarkController.value),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(100),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'$studentName • $studentPhone',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white.withAlpha(90),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Video Controls Overlay (Bottom)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Color(0xE6000000)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Socratic Timeline Progress Bar with Checkpoint Markers
|
||||
Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 5,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
|
||||
activeTrackColor: AppColors.saqelCyan,
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: AppColors.saqelCyan,
|
||||
),
|
||||
child: Slider(
|
||||
value: progress,
|
||||
onChanged: (val) {
|
||||
final targetSec = (val * duration).toInt();
|
||||
context.read<VideoPlaybackCubit>().seekTo(targetSec);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Checkpoint markers on the progress track
|
||||
...state.playbackData.checkpoints.map((cp) {
|
||||
final posFraction = (cp.timestampSeconds / duration).clamp(0.0, 1.0);
|
||||
final isPassed = state.passedCheckpointIds.contains(cp.id);
|
||||
return Positioned(
|
||||
left: (MediaQuery.of(context).size.width * 0.85 * posFraction).clamp(20.0, 700.0),
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber).withAlpha(120),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
state.isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => context.read<VideoPlaybackCubit>().togglePlayPause(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${_formatTime(current)} / ${_formatTime(duration)}',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${state.passedCheckpointIds.length}/${state.playbackData.checkpoints.length} فحص منجز',
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Remediation Feedback Alert (if triggered)
|
||||
if (state.remediationNotice != null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.remediationNotice!,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Lesson Meta & Learning Outcomes
|
||||
LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'نتاجات التعلم والمفاهيم الوزارية 🎯',
|
||||
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.appleBlue.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
widget.subject?.title ?? 'المنهاج الرسمي',
|
||||
style: const TextStyle(color: AppColors.appleBlue, fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
if (widget.lesson.outcomes.isNotEmpty)
|
||||
...widget.lesson.outcomes.map((out) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.check_mark_circled_solid, color: AppColors.saqelCyan, size: 16),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
out,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 13, height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
else
|
||||
const Text(
|
||||
'شرح المفاهيم والقواعد الوزارية المقررة مع التطبيقات الحياتية ونماذج الامتحانات.',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -120,15 +120,17 @@ class LuxuryTextField extends StatelessWidget {
|
||||
final TextInputType keyboardType;
|
||||
final Widget? prefixIcon;
|
||||
final bool enabled;
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
const LuxuryTextField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.label,
|
||||
this.label = '',
|
||||
required this.hint,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.prefixIcon,
|
||||
this.enabled = true,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -136,18 +138,21 @@ class LuxuryTextField extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.titleMedium.copyWith(
|
||||
color: AppColors.textSecondaryDark,
|
||||
fontSize: 13,
|
||||
if (label.isNotEmpty) ...[
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.titleMedium.copyWith(
|
||||
color: AppColors.textSecondaryDark,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
enabled: enabled,
|
||||
onChanged: onChanged,
|
||||
style: AppTypography.bodyMedium.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../data/models/socratic_checkpoint_model.dart';
|
||||
import 'luxury_widgets.dart';
|
||||
|
||||
class SocraticCheckpointModal extends StatefulWidget {
|
||||
final SocraticCheckpointModel checkpoint;
|
||||
final Function(SocraticOptionModel) onOptionSelected;
|
||||
|
||||
const SocraticCheckpointModal({
|
||||
super.key,
|
||||
required this.checkpoint,
|
||||
required this.onOptionSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SocraticCheckpointModal> createState() => _SocraticCheckpointModalState();
|
||||
}
|
||||
|
||||
class _SocraticCheckpointModalState extends State<SocraticCheckpointModal> {
|
||||
SocraticOptionModel? _selectedOption;
|
||||
bool _submitted = false;
|
||||
bool _showHint = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 540),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0C1424),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(90), width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.saqelCyan.withAlpha(40),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
const BoxShadow(
|
||||
color: Colors.black,
|
||||
blurRadius: 30,
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Top Badge: Socratic Gatekeeping Indicator
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'فحص الفهم السقراطي الذكي 🧠',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.saqelCyan,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'+0.5% جاهزية',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.emeraldGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Question Text
|
||||
Text(
|
||||
widget.checkpoint.questionText,
|
||||
style: AppTypography.titleLarge.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.4,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Options List
|
||||
...widget.checkpoint.options.map((opt) {
|
||||
final isSelected = _selectedOption?.id == opt.id;
|
||||
Color borderColor = AppColors.darkCardBorder;
|
||||
Color bgColor = AppColors.darkSurface;
|
||||
|
||||
if (_submitted) {
|
||||
if (opt.isCorrect) {
|
||||
borderColor = AppColors.emeraldGreen;
|
||||
bgColor = AppColors.emeraldGreen.withAlpha(30);
|
||||
} else if (isSelected && !opt.isCorrect) {
|
||||
borderColor = AppColors.crimsonRed;
|
||||
bgColor = AppColors.crimsonRed.withAlpha(30);
|
||||
}
|
||||
} else if (isSelected) {
|
||||
borderColor = AppColors.saqelCyan;
|
||||
bgColor = AppColors.saqelCyan.withAlpha(20);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: InkWell(
|
||||
onTap: _submitted
|
||||
? null
|
||||
: () {
|
||||
setState(() => _selectedOption = opt);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: borderColor, width: isSelected ? 1.8 : 1.0),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.saqelCyan : AppColors.textSecondaryDark,
|
||||
width: 2,
|
||||
),
|
||||
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(Icons.check, size: 16, color: Colors.black)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
opt.text,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.white : AppColors.textSecondaryDark,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Hint Drawer (if requested)
|
||||
if (_showHint && widget.checkpoint.hint.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.guardianAmber.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.guardianAmber.withAlpha(60)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.lightbulb_fill, color: AppColors.guardianAmber, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.checkpoint.hint,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFDE68A),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.4,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Bottom Actions
|
||||
Row(
|
||||
children: [
|
||||
if (!_showHint && widget.checkpoint.hint.isNotEmpty)
|
||||
TextButton.icon(
|
||||
onPressed: () => setState(() => _showHint = true),
|
||||
icon: const Icon(CupertinoIcons.lightbulb, size: 16, color: AppColors.guardianAmber),
|
||||
label: const Text(
|
||||
'تلميح الذكاء الاصطناعي',
|
||||
style: TextStyle(color: AppColors.guardianAmber, fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: LuxuryButton(
|
||||
text: _submitted ? 'متابعة ⚡' : 'تأكيد الإجابة 🚀',
|
||||
onPressed: _selectedOption == null
|
||||
? null
|
||||
: () {
|
||||
if (!_submitted) {
|
||||
setState(() => _submitted = true);
|
||||
widget.onOptionSelected(_selectedOption!);
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user