fix(platform): grade-gate student app, isolate per-video resume & checkpoints, remove fake teachers, enhance arabic lab, clean flutter analyze
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
/// Directorate Model (المظلة المركزية: مديرية الثقافة العسكرية)
|
/// Directorate Model (المظلة المركزية: مديرية الثقافة العسكرية)
|
||||||
class DirectorateOverview {
|
class DirectorateOverview {
|
||||||
final String code;
|
final String code;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class _SchoolPrincipalScreenState extends State<SchoolPrincipalScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showDualFormsPreview() async {
|
void _showDualFormsPreview() async {
|
||||||
final data = await DirectorateApiService.fetchDualForms();
|
await DirectorateApiService.fetchDualForms();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
|
|||||||
@@ -316,10 +316,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -332,10 +332,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -561,10 +561,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.11"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class CurriculumBakedData {
|
|||||||
#### البند 1: حل المعادلات بإخراج العامل المشترك الأكبر
|
#### البند 1: حل المعادلات بإخراج العامل المشترك الأكبر
|
||||||
- **قاعدة:** نجعل طرف المعادلة الأيمن صفراً، ثم نخرج العامل المشترك، ونحل المعادلة الناتجة.
|
- **قاعدة:** نجعل طرف المعادلة الأيمن صفراً، ثم نخرج العامل المشترك، ونحل المعادلة الناتجة.
|
||||||
- **مثال 1:** حل المعادلة: \$x^3 + 4x^2 = 5x\$
|
- **مثال 1:** حل المعادلة: \$x^3 + 4x^2 = 5x\$
|
||||||
$$\$x^3 + 4x^2 - 5x = 0 \\implies x(x^2 + 4x - 5) = 0 \\implies x(x + 5)(x - 1) = 0\$$
|
\$\$x^3 + 4x^2 - 5x = 0 \\implies x(x^2 + 4x - 5) = 0 \\implies x(x + 5)(x - 1) = 0\$\$
|
||||||
إذن جذور المعادلة هي: \$\\{-5, 0, 1\\}\$.
|
إذن جذور المعادلة هي: \$\\{-5, 0, 1\\}\$.
|
||||||
|
|
||||||
- **أتحقق من فهمي (صفحة 9):**
|
- **أتحقق من فهمي (صفحة 9):**
|
||||||
@@ -97,10 +97,10 @@ class CurriculumBakedData {
|
|||||||
- **المفهوم:** نقطة تقاطع المستقيم والمنحنى تمثل حل النظام المشترك.
|
- **المفهوم:** نقطة تقاطع المستقيم والمنحنى تمثل حل النظام المشترك.
|
||||||
- **طريقة التعويض:** نجعل أحد المتغيرين موضوعاً للقانون في المعادلة الخطية ونعوضه في المعادلة التربيعية.
|
- **طريقة التعويض:** نجعل أحد المتغيرين موضوعاً للقانون في المعادلة الخطية ونعوضه في المعادلة التربيعية.
|
||||||
- **مثال محلول:**
|
- **مثال محلول:**
|
||||||
$$\\begin{cases} y - x = 1 \\\\ x^2 + y^2 = 25 \\end{cases}$$
|
\$\$\\begin{cases} y - x = 1 \\\\ x^2 + y^2 = 25 \\end{cases}\$\$
|
||||||
من المعادلة الأولى: \$y = x + 1\$. نعوض في الثانية:
|
من المعادلة الأولى: \$y = x + 1\$. نعوض في الثانية:
|
||||||
$$\$x^2 + (x + 1)^2 = 25 \\implies 2x^2 + 2x - 24 = 0 \\implies x^2 + x - 12 = 0\$$
|
\$\$x^2 + (x + 1)^2 = 25 \\implies 2x^2 + 2x - 24 = 0 \\implies x^2 + x - 12 = 0\$\$
|
||||||
$$\$(x + 4)(x - 3) = 0 \\implies x = 3 \\text{ أو } x = -4\$$
|
\$\$(x + 4)(x - 3) = 0 \\implies x = 3 \\text{ أو } x = -4\$\$
|
||||||
- عندما \$x = 3 \\implies y = 4\$ (النقطة: \$(3, 4)\$)
|
- عندما \$x = 3 \\implies y = 4\$ (النقطة: \$(3, 4)\$)
|
||||||
- عندما \$x = -4 \\implies y = -3\$ (النقطة: \$( -4, -3)\$)
|
- عندما \$x = -4 \\implies y = -3\$ (النقطة: \$( -4, -3)\$)
|
||||||
|
|
||||||
@@ -144,11 +144,11 @@ class CurriculumBakedData {
|
|||||||
---
|
---
|
||||||
|
|
||||||
### السؤال الثاني: حل النظام الجبري التالي وتحقق من صحة الحل (6 علامات)
|
### السؤال الثاني: حل النظام الجبري التالي وتحقق من صحة الحل (6 علامات)
|
||||||
$$\\begin{cases} y = 2x + 3 \\\\ x^2 + y = 6 \\end{cases}$$
|
\$\$\\begin{cases} y = 2x + 3 \\\\ x^2 + y = 6 \\end{cases}\$\$
|
||||||
|
|
||||||
**خطوات الإجابة:**
|
**خطوات الإجابة:**
|
||||||
- نعوض \$y = 2x + 3\$ في المعادلة الثانية:
|
- نعوض \$y = 2x + 3\$ في المعادلة الثانية:
|
||||||
$$\$x^2 + 2x + 3 = 6 \\implies x^2 + 2x - 3 = 0\$$
|
\$\$x^2 + 2x + 3 = 6 \\implies x^2 + 2x - 3 = 0\$\$
|
||||||
- التحليل: \$(x + 3)(x - 1) = 0\$.
|
- التحليل: \$(x + 3)(x - 1) = 0\$.
|
||||||
- إذا \$x = 1 \\implies y = 2(1) + 3 = 5\$ ➔ الحل الأول: \$(1, 5)\$.
|
- إذا \$x = 1 \\implies y = 2(1) + 3 = 5\$ ➔ الحل الأول: \$(1, 5)\$.
|
||||||
- إذا \$x = -3 \\implies y = 2(-3) + 3 = -3\$ ➔ الحل الثاني: \$( -3, -3)\$.
|
- إذا \$x = -3 \\implies y = 2(-3) + 3 = -3\$ ➔ الحل الثاني: \$( -3, -3)\$.
|
||||||
@@ -193,12 +193,12 @@ $$\\begin{cases} y = 2x + 3 \\\\ x^2 + y = 6 \\end{cases}$$
|
|||||||
|
|
||||||
### ضرب المتجهات (Vector Multiplication):
|
### ضرب المتجهات (Vector Multiplication):
|
||||||
1. **الضرب القياسي / النقطي (Dot Product):**
|
1. **الضرب القياسي / النقطي (Dot Product):**
|
||||||
$$\\vec{A} \\cdot \\vec{B} = |A| |B| \\cos\\phi$$
|
\$\$\\vec{A} \\cdot \\vec{B} = |A| |B| \\cos\\phi\$\$
|
||||||
- الناتج كمية قياسية (مثل حساب الشغل: \$W = \\vec{F} \\cdot \\vec{d}\$).
|
- الناتج كمية قياسية (مثل حساب الشغل: \$W = \\vec{F} \\cdot \\vec{d}\$).
|
||||||
- ينعدم الضرب القياسي إذا كان المتجهان متعامدين (\$\\phi = 90^\\circ\$ لأن \$\\cos 90^\\circ = 0\$).
|
- ينعدم الضرب القياسي إذا كان المتجهان متعامدين (\$\\phi = 90^\\circ\$ لأن \$\\cos 90^\\circ = 0\$).
|
||||||
|
|
||||||
2. **الضرب المتجهي / التقاطعي (Cross Product):**
|
2. **الضرب المتجهي / التقاطعي (Cross Product):**
|
||||||
$$|\\vec{A} \\times \\vec{B}| = |A| |B| \\sin\\phi$$
|
\$\$|\\vec{A} \\times \\vec{B}| = |A| |B| \\sin\\phi\$\$
|
||||||
- الناتج متجه عمودي على المستوى الذي يحوي المتجهين، ويُحدد اتجاهه باستخدام قاعدة اليد اليمنى.
|
- الناتج متجه عمودي على المستوى الذي يحوي المتجهين، ويُحدد اتجاهه باستخدام قاعدة اليد اليمنى.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ $$\\begin{cases} y = 2x + 3 \\\\ x^2 + y = 6 \\end{cases}$$
|
|||||||
2. تعليق كتلة \$m_2 = 200\\text{ g}\$ عند زاوية \$90^\\circ\$.
|
2. تعليق كتلة \$m_2 = 200\\text{ g}\$ عند زاوية \$90^\\circ\$.
|
||||||
3. اتزان الحلقة في المنتصف بوضع قوة موازنة ثالثة في الربع المقابل بمقدار \$250\\text{ g}\$ وزاوية \$233^\\circ\$.
|
3. اتزان الحلقة في المنتصف بوضع قوة موازنة ثالثة في الربع المقابل بمقدار \$250\\text{ g}\$ وزاوية \$233^\\circ\$.
|
||||||
4. **الاستنتاج:** محصلة قوتين متعامدتين تُحسب بفيثاغورس:
|
4. **الاستنتاج:** محصلة قوتين متعامدتين تُحسب بفيثاغورس:
|
||||||
$$R = \\sqrt{F_1^2 + F_2^2} = \\sqrt{150^2 + 200^2} = 250\\text{ gf}$$
|
\$\$R = \\sqrt{F_1^2 + F_2^2} = \\sqrt{150^2 + 200^2} = 250\\text{ gf}\$\$
|
||||||
""";
|
""";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -224,20 +224,36 @@ class CurriculumLessonItemModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final filePath = json['file']?.toString();
|
final filePath = json['file']?.toString();
|
||||||
// Default video availability: true for core Grade 10 subjects (Math, Physics, English) and explicitly tagged lessons
|
final jsonId = json['id']?.toString();
|
||||||
final bool isCoreTenthSubjectLesson = filePath != null && (
|
|
||||||
filePath.contains('math_10/semester_1') ||
|
// Stable, deterministic lesson ID (vital for persistent per-lesson progress tracking)
|
||||||
filePath.contains('physics_10/semester_1') ||
|
final stableId = (jsonId != null && jsonId.isNotEmpty)
|
||||||
filePath.contains('english_10/semester_1') ||
|
? jsonId
|
||||||
filePath.contains('lesson_01') ||
|
: (filePath != null && filePath.isNotEmpty
|
||||||
filePath.contains('lesson_1a')
|
? filePath.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_')
|
||||||
|
: 'lesson_${(json['title']?.toString() ?? 'default').hashCode.abs()}');
|
||||||
|
|
||||||
|
// Video availability:
|
||||||
|
// 1. Explicitly false for introductory/project overviews
|
||||||
|
// 2. Physics & other subjects currently have no video -> false
|
||||||
|
// 3. Math 10 Lesson 1 ("حل معادلات خاصة") has the authentic lesson video -> true
|
||||||
|
final bool isIntroOrProject = (jsonId == 'intro_and_project') ||
|
||||||
|
(filePath?.contains('intro_and_project') ?? false) ||
|
||||||
|
((json['title']?.toString() ?? '').contains('مقدمة ومشروع'));
|
||||||
|
|
||||||
|
final bool isMath10Lesson1 = !isIntroOrProject && (
|
||||||
|
(filePath != null && filePath.contains('math_10/semester_1/unit_01/lesson_01')) ||
|
||||||
|
((json['title']?.toString() ?? '').contains('معادلاتٍ خاصّةٍ'))
|
||||||
);
|
);
|
||||||
|
|
||||||
final bool hasVideoExplicit = (json['has_video'] as bool?) ??
|
final bool hasVideoExplicit = !isIntroOrProject && (
|
||||||
((json['video_url'] != null && json['video_url'].toString().isNotEmpty) || isCoreTenthSubjectLesson);
|
(json['has_video'] == true) ||
|
||||||
|
(json['video_url'] != null && json['video_url'].toString().isNotEmpty) ||
|
||||||
|
isMath10Lesson1
|
||||||
|
);
|
||||||
|
|
||||||
return CurriculumLessonItemModel(
|
return CurriculumLessonItemModel(
|
||||||
id: json['id']?.toString() ?? 'lesson_${DateTime.now().millisecondsSinceEpoch}',
|
id: stableId,
|
||||||
title: json['title']?.toString() ?? 'درس بدون عنوان',
|
title: json['title']?.toString() ?? 'درس بدون عنوان',
|
||||||
outcomes: outs,
|
outcomes: outs,
|
||||||
markdownFilePath: filePath,
|
markdownFilePath: filePath,
|
||||||
|
|||||||
@@ -76,19 +76,34 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
|||||||
: _repo = repo ?? CurriculumRepository(),
|
: _repo = repo ?? CurriculumRepository(),
|
||||||
super(VideoPlaybackInitial());
|
super(VideoPlaybackInitial());
|
||||||
|
|
||||||
|
String _getLessonStorageKey(CurriculumLessonItemModel? lesson, {String? fallbackId}) {
|
||||||
|
if (lesson?.markdownFilePath != null && lesson!.markdownFilePath!.isNotEmpty) {
|
||||||
|
return lesson.markdownFilePath!.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
|
||||||
|
}
|
||||||
|
if (lesson?.id != null && lesson!.id.isNotEmpty) {
|
||||||
|
return lesson.id.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
|
||||||
|
}
|
||||||
|
return fallbackId ?? 'lesson_default';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async {
|
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async {
|
||||||
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
|
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
|
||||||
_lastObservedPosition = -1;
|
_lastObservedPosition = -1;
|
||||||
emit(VideoPlaybackLoading());
|
emit(VideoPlaybackLoading());
|
||||||
|
final storageKey = _getLessonStorageKey(lesson);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id);
|
final playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id);
|
||||||
|
|
||||||
// Load saved resume position
|
// Load saved resume position strictly per video
|
||||||
int resumePos = playback.lastPositionSeconds ?? 0;
|
int resumePos = playback.lastPositionSeconds ?? 0;
|
||||||
|
Set<int> passedIds = {};
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final localPos = prefs.getInt('saved_video_pos_${lesson.id}') ?? 0;
|
final localPos = prefs.getInt('saved_video_pos_$storageKey') ?? 0;
|
||||||
if (localPos > resumePos) resumePos = localPos;
|
if (localPos > resumePos) resumePos = localPos;
|
||||||
|
final savedPassed = prefs.getStringList('passed_checkpoints_$storageKey') ?? [];
|
||||||
|
passedIds = savedPassed.map((s) => int.tryParse(s) ?? 0).where((id) => id > 0).toSet();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
emit(VideoPlaybackReady(
|
emit(VideoPlaybackReady(
|
||||||
@@ -96,18 +111,22 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
|||||||
lessonItem: lesson,
|
lessonItem: lesson,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
currentPositionSeconds: resumePos,
|
currentPositionSeconds: resumePos,
|
||||||
|
passedCheckpointIds: passedIds,
|
||||||
isPlaying: true,
|
isPlaying: true,
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppLogger.log('Playback API unavailable (${lesson.title}): $e — Switching to offline Socratic fallback', tag: 'VIDEO_CUBIT');
|
AppLogger.log('Playback API unavailable (${lesson.title}): $e — Switching to offline Socratic fallback', tag: 'VIDEO_CUBIT');
|
||||||
final fallback = _buildResilientLessonPlayback(lesson, subject: subject);
|
final fallback = _buildResilientLessonPlayback(lesson, subject: subject);
|
||||||
|
|
||||||
// Load saved resume position
|
// Load saved resume position strictly per video
|
||||||
int resumePos = fallback.lastPositionSeconds ?? 0;
|
int resumePos = fallback.lastPositionSeconds ?? 0;
|
||||||
|
Set<int> passedIds = {};
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final localPos = prefs.getInt('saved_video_pos_${lesson.id}') ?? 0;
|
final localPos = prefs.getInt('saved_video_pos_$storageKey') ?? 0;
|
||||||
if (localPos > resumePos) resumePos = localPos;
|
if (localPos > resumePos) resumePos = localPos;
|
||||||
|
final savedPassed = prefs.getStringList('passed_checkpoints_$storageKey') ?? [];
|
||||||
|
passedIds = savedPassed.map((s) => int.tryParse(s) ?? 0).where((id) => id > 0).toSet();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
emit(VideoPlaybackReady(
|
emit(VideoPlaybackReady(
|
||||||
@@ -115,6 +134,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
|||||||
lessonItem: lesson,
|
lessonItem: lesson,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
currentPositionSeconds: resumePos,
|
currentPositionSeconds: resumePos,
|
||||||
|
passedCheckpointIds: passedIds,
|
||||||
isPlaying: true,
|
isPlaying: true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -197,21 +217,12 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
|||||||
LessonVersionModel(
|
LessonVersionModel(
|
||||||
lessonId: 101,
|
lessonId: 101,
|
||||||
isAi: true,
|
isAi: true,
|
||||||
teacherName: 'منصة صَقِل الذكية 🤖',
|
teacherName: 'منصة صَقِل التعليمية المعتمدة',
|
||||||
schoolName: 'المركز الرقمي المعتمد',
|
schoolName: 'المركز الرقمي المعتمد',
|
||||||
label: 'شرح الذكاء الاصطناعي الرسمي',
|
label: 'الشرح الرقمي الرسمي المعتمد',
|
||||||
isRecommended: true,
|
isRecommended: true,
|
||||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||||
),
|
),
|
||||||
LessonVersionModel(
|
|
||||||
lessonId: 102,
|
|
||||||
isAi: false,
|
|
||||||
teacherName: 'أ. أحمد المجالي 👨🏫',
|
|
||||||
schoolName: 'مدارس الثقافة العسكرية',
|
|
||||||
label: 'شرح معلم معتمد 🌟 4.9',
|
|
||||||
isRecommended: false,
|
|
||||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -279,12 +290,16 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
|||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is! VideoPlaybackReady) return;
|
if (currentState is! VideoPlaybackReady) return;
|
||||||
try {
|
try {
|
||||||
|
final storageKey = _getLessonStorageKey(currentState.lessonItem, fallbackId: '${currentState.playbackData.lessonId}');
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (currentState.lessonItem != null) {
|
await prefs.setInt('saved_video_pos_$storageKey', positionSeconds);
|
||||||
await prefs.setInt('saved_video_pos_${currentState.lessonItem!.id}', positionSeconds);
|
if (currentState.passedCheckpointIds.isNotEmpty) {
|
||||||
|
await prefs.setStringList(
|
||||||
|
'passed_checkpoints_$storageKey',
|
||||||
|
currentState.passedCheckpointIds.map((id) => id.toString()).toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (currentState.playbackData.lessonId > 0) {
|
if (currentState.playbackData.lessonId > 0) {
|
||||||
await prefs.setInt('saved_video_pos_${currentState.playbackData.lessonId}', positionSeconds);
|
|
||||||
await _repo.saveProgress(
|
await _repo.saveProgress(
|
||||||
lessonId: currentState.playbackData.lessonId,
|
lessonId: currentState.playbackData.lessonId,
|
||||||
positionSeconds: positionSeconds,
|
positionSeconds: positionSeconds,
|
||||||
@@ -307,6 +322,13 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
|||||||
AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE');
|
AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE');
|
||||||
final updatedPassed = Set<int>.from(currentState.passedCheckpointIds)..add(cp.id);
|
final updatedPassed = Set<int>.from(currentState.passedCheckpointIds)..add(cp.id);
|
||||||
_lastObservedPosition = cp.timestampSeconds;
|
_lastObservedPosition = cp.timestampSeconds;
|
||||||
|
|
||||||
|
// Persist passed checkpoint for this specific video
|
||||||
|
final storageKey = _getLessonStorageKey(currentState.lessonItem, fallbackId: '${currentState.playbackData.lessonId}');
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setStringList('passed_checkpoints_$storageKey', updatedPassed.map((id) => id.toString()).toList());
|
||||||
|
}).catchError((_) {});
|
||||||
|
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
clearActiveCheckpoint: true,
|
clearActiveCheckpoint: true,
|
||||||
isPlaying: true,
|
isPlaying: true,
|
||||||
|
|||||||
+103
-5
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
|
||||||
|
|
||||||
/// ==============================================================================
|
/// ==============================================================================
|
||||||
/// SAQEL ENTERPRISE - ARABIC INTERACTIVE LAB (شجرة الإعراب ومختبر العروض)
|
/// SAQEL ENTERPRISE - ARABIC INTERACTIVE LAB (شجرة الإعراب ومختبر العروض)
|
||||||
@@ -140,12 +139,109 @@ class _ArabicInteractiveLabViewState extends State<ArabicInteractiveLabView>
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'fullText': 'المُعَلِّمُ المُخْلِصُ مُحْتَرَمٌ قَدْرُهُ بَيْنَ النَّاسِ',
|
||||||
|
'type': 'جملة اسمية تشتمل على اسم فاعل واسم مفعول وعملهما',
|
||||||
|
'rootNode': 'الجملة الاسمية والمشتقات العاملة (الصف العاشر)',
|
||||||
|
'words': [
|
||||||
|
{
|
||||||
|
'word': 'المُعَلِّمُ',
|
||||||
|
'role': 'مبتدأ مرفوع (اسم فاعل من غير الثلاثي)',
|
||||||
|
'case': 'مرفوع وعلامة رفعه الضمة الظاهرة على آخره',
|
||||||
|
'explanation': 'اسم معرف بأل وقع في أول الجملة، وهو اسم فاعل من الفعل علّم يُعلّم.',
|
||||||
|
'tag': 'مبتدأ / اسم فاعل',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'المُخْلِصُ',
|
||||||
|
'role': 'نعت (صفة) للمبتدأ',
|
||||||
|
'case': 'مرفوع وعلامة رفعه الضمة الظاهرة',
|
||||||
|
'explanation': 'نعت حقيقي طابق المنعوت في التذكير والتعريف والإعراب.',
|
||||||
|
'tag': 'نعت مرفوع',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'مُحْتَرَمٌ',
|
||||||
|
'role': 'خبر المبتدأ (اسم مفعول عامل)',
|
||||||
|
'case': 'مرفوع وعلامة رفعه الضمة الظاهرة',
|
||||||
|
'explanation': 'خبر أتم معنى الجملة، وهو مشتق (اسم مفعول) يعمل عمل فعله المبني للمجهول.',
|
||||||
|
'tag': 'خبر / اسم مفعول',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'قَدْرُهُ',
|
||||||
|
'role': 'نائب فاعل لاسم المفعول (مُحترَم)',
|
||||||
|
'case': 'مرفوع بالضمة، والهاء ضمير متصل مبني في محل جر مضاف إليه',
|
||||||
|
'explanation': 'اسم المفعول يعمل عمل فعله المبني للمجهول فيرفع نائب فاعل بعده.',
|
||||||
|
'tag': 'نائب فاعل',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'بَيْنَ',
|
||||||
|
'role': 'ظرف مكان منصوب (مفعول فيه)',
|
||||||
|
'case': 'منصوب بالفتحة وهو مضاف',
|
||||||
|
'explanation': 'ظرف مكان يحدد موقع الاتصاف بالاحترام.',
|
||||||
|
'tag': 'ظرف مكان',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'النَّاسِ',
|
||||||
|
'role': 'مضاف إليه مجرور',
|
||||||
|
'case': 'مجرور وعلامة جره الكسرة الظاهرة على آخره',
|
||||||
|
'explanation': 'اسم مجرور بالإضافة جاء بعد ظرف المكان.',
|
||||||
|
'tag': 'مضاف إليه',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fullText': 'مَنْ يَزْرَعْ خَيْراً يَحْصُدْ ثِمَاراً طَيِّبَةً',
|
||||||
|
'type': 'أسلوب شرط جازم (منهاج العاشر الأساسي)',
|
||||||
|
'rootNode': 'أسلوب الشرط (اسم شرط + فعل الشرط + جواب الشرط)',
|
||||||
|
'words': [
|
||||||
|
{
|
||||||
|
'word': 'مَنْ',
|
||||||
|
'role': 'اسم شرط جازم مبني',
|
||||||
|
'case': 'مبني على السكون في محل رفع مبتدأ',
|
||||||
|
'explanation': 'اسم شرط جازم يدل على العاقل ويربط بين جملتين.',
|
||||||
|
'tag': 'اسم شرط',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'يَزْرَعْ',
|
||||||
|
'role': 'فعل الشرط مضارع مجزوم',
|
||||||
|
'case': 'مجزوم بالسكون، والفاعل ضمير مستتر تقديره (هو)',
|
||||||
|
'explanation': 'فعل مضارع وقع بعد أداة الشرط الجازمة، فجُزم بالسكون.',
|
||||||
|
'tag': 'فعل شرط مجزوم',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'خَيْراً',
|
||||||
|
'role': 'مفعول به لفعل الشرط',
|
||||||
|
'case': 'منصوب وعلامة نصبه تنوين الفتح',
|
||||||
|
'explanation': 'وقع عليه فعل الزراعة المعنوي.',
|
||||||
|
'tag': 'مفعول به',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'يَحْصُدْ',
|
||||||
|
'role': 'جواب الشرط وجزاؤه مضارع مجزوم',
|
||||||
|
'case': 'مجزوم بالسكون، والفاعل مستتر تقديره (هو)',
|
||||||
|
'explanation': 'فعل مضارع جُزم لوقوعه جواباً للشرط.',
|
||||||
|
'tag': 'جواب شرط مجزوم',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'ثِمَاراً',
|
||||||
|
'role': 'مفعول به لجواب الشرط',
|
||||||
|
'case': 'منصوب وعلامة نصبه تنوين الفتح الظاهر',
|
||||||
|
'explanation': 'الشيء المحصود نتيجة العمل.',
|
||||||
|
'tag': 'مفعول به',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'word': 'طَيِّبَةً',
|
||||||
|
'role': 'نعت لثماراً',
|
||||||
|
'case': 'منصوب وعلامة نصبه تنوين الفتح',
|
||||||
|
'explanation': 'صفة طابقت موصوفها في التنكير والجمع والتأنيث والنصب.',
|
||||||
|
'tag': 'نعت منصوب',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Poetry Meter State
|
// Poetry Meter State
|
||||||
int _selectedMeterIndex = 0;
|
int _selectedMeterIndex = 0;
|
||||||
bool _isPlayingBeat = false;
|
bool _isPlayingBeat = false;
|
||||||
int _currentBeatStep = 0;
|
|
||||||
|
|
||||||
final List<Map<String, dynamic>> _poetryMeters = [
|
final List<Map<String, dynamic>> _poetryMeters = [
|
||||||
{
|
{
|
||||||
@@ -186,7 +282,9 @@ class _ArabicInteractiveLabViewState extends State<ArabicInteractiveLabView>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this)..addListener(() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -339,8 +437,8 @@ class _ArabicInteractiveLabViewState extends State<ArabicInteractiveLabView>
|
|||||||
_selectedWordIndex = null;
|
_selectedWordIndex = null;
|
||||||
}),
|
}),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.only(left: idx == 0 ? 8 : 0),
|
margin: const EdgeInsets.only(left: 8),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isCur
|
color: isCur
|
||||||
? const Color(0xFF1E293B)
|
? const Color(0xFF1E293B)
|
||||||
|
|||||||
-1
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
|
||||||
|
|
||||||
/// ==============================================================================
|
/// ==============================================================================
|
||||||
/// SAQEL ENTERPRISE - JORDAN HISTORY INTERACTIVE TIMELINE (الخط الزمني لتاريخ الأردن)
|
/// SAQEL ENTERPRISE - JORDAN HISTORY INTERACTIVE TIMELINE (الخط الزمني لتاريخ الأردن)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
|
||||||
|
|
||||||
/// ==============================================================================
|
/// ==============================================================================
|
||||||
/// SAQEL ENTERPRISE - ISLAMIC INHERITANCE & TAJWEED LAB (حاسبة المواريث والتجويد)
|
/// SAQEL ENTERPRISE - ISLAMIC INHERITANCE & TAJWEED LAB (حاسبة المواريث والتجويد)
|
||||||
|
|||||||
@@ -787,211 +787,21 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Multi-Teacher and AI Video Selector Bottom Sheet
|
/// Direct Launch of Socratic Video Player or Transparent No-Video Sheet
|
||||||
void _showLessonVideoSelector(BuildContext context, CurriculumLessonItemModel lesson) {
|
void _showLessonVideoSelector(BuildContext context, CurriculumLessonItemModel lesson) {
|
||||||
if (!lesson.hasVideo) {
|
if (!lesson.hasVideo) {
|
||||||
_showNoVideoAvailableSheet(context, lesson);
|
_showNoVideoAvailableSheet(context, lesson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: AppColors.darkSurface,
|
|
||||||
isScrollControlled: true,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
||||||
),
|
|
||||||
builder: (ctx) => SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white24,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.saqelCyan.withAlpha(25),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Icon(CupertinoIcons.play_circle_fill, color: AppColors.saqelCyan, size: 24),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'اختر أسلوب الشرح المناسب لك 🎥',
|
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 3),
|
|
||||||
Text(
|
|
||||||
lesson.title,
|
|
||||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
|
|
||||||
// Option 1: Official Saqel AI Master Lesson
|
// Direct authentic launch: zero mockups, zero fake teacher names
|
||||||
_buildVideoOptionCard(
|
Navigator.of(context).push(
|
||||||
title: 'شرح الذكاء الاصطناعي لمنصة صَقِل 🤖',
|
CupertinoPageRoute(
|
||||||
subtitle: 'الشرح الرسمي المعتمد وتفكيك المفاهيم خطوة بخطوة',
|
builder: (_) => SocraticVideoPlayerScreen(
|
||||||
tag: 'المنهاج المعتمد + سقراطي',
|
lesson: lesson,
|
||||||
tagColor: AppColors.saqelCyan,
|
subject: widget.subject,
|
||||||
icon: CupertinoIcons.sparkles,
|
instructorName: 'منصة صَقِل التعليمية المعتمدة',
|
||||||
iconColor: AppColors.saqelCyan,
|
videoSourceLabel: 'الشرح الرقمي الرسمي المعتمد',
|
||||||
onTap: () {
|
|
||||||
Navigator.of(ctx).pop();
|
|
||||||
Navigator.of(context).push(
|
|
||||||
CupertinoPageRoute(
|
|
||||||
builder: (_) => SocraticVideoPlayerScreen(
|
|
||||||
lesson: lesson,
|
|
||||||
subject: widget.subject,
|
|
||||||
instructorName: 'منصة صَقِل الذكية 🤖',
|
|
||||||
videoSourceLabel: 'شرح الذكاء الاصطناعي الرسمي',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// Option 2: Certified Teacher 1
|
|
||||||
_buildVideoOptionCard(
|
|
||||||
title: 'شرح الأستاذ رعد الخمايسة 👨🏫',
|
|
||||||
subtitle: 'خبير التوجيهي — حل نماذج وأسئلة وزارية إضافية',
|
|
||||||
tag: 'معلم معتمد 🌟 4.9',
|
|
||||||
tagColor: AppColors.guardianAmber,
|
|
||||||
icon: CupertinoIcons.person_crop_circle_badge_checkmark,
|
|
||||||
iconColor: AppColors.guardianAmber,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(ctx).pop();
|
|
||||||
Navigator.of(context).push(
|
|
||||||
CupertinoPageRoute(
|
|
||||||
builder: (_) => SocraticVideoPlayerScreen(
|
|
||||||
lesson: lesson,
|
|
||||||
subject: widget.subject,
|
|
||||||
instructorName: 'أ. رعد الخمايسة (معلم معتمد)',
|
|
||||||
videoSourceLabel: 'شرح الأستاذ رعد الخمايسة',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// Option 3: Certified Teacher 2
|
|
||||||
_buildVideoOptionCard(
|
|
||||||
title: 'شرح الأستاذ إياد النجار 👨🏫',
|
|
||||||
subtitle: 'كبير المعلمين — تبسيط المفاهيم المعقدة والتحليل البياني',
|
|
||||||
tag: 'معلم معتمد 🌟 4.8',
|
|
||||||
tagColor: AppColors.emeraldGreen,
|
|
||||||
icon: CupertinoIcons.person_crop_circle_fill,
|
|
||||||
iconColor: AppColors.emeraldGreen,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(ctx).pop();
|
|
||||||
Navigator.of(context).push(
|
|
||||||
CupertinoPageRoute(
|
|
||||||
builder: (_) => SocraticVideoPlayerScreen(
|
|
||||||
lesson: lesson,
|
|
||||||
subject: widget.subject,
|
|
||||||
instructorName: 'أ. إياد النجار (كبير المعلمين)',
|
|
||||||
videoSourceLabel: 'شرح الأستاذ إياد النجار',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildVideoOptionCard({
|
|
||||||
required String title,
|
|
||||||
required String subtitle,
|
|
||||||
required String tag,
|
|
||||||
required Color tagColor,
|
|
||||||
required IconData icon,
|
|
||||||
required Color iconColor,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.darkCard,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
border: Border.all(color: AppColors.darkCardBorder),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: iconColor.withAlpha(25),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Icon(icon, color: iconColor, size: 22),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13.5),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: tagColor.withAlpha(25),
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
tag,
|
|
||||||
style: TextStyle(color: tagColor, fontSize: 10, fontWeight: FontWeight.w700),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
subtitle,
|
|
||||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Icon(CupertinoIcons.chevron_back, color: AppColors.textSecondaryDark, size: 14),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -188,45 +188,42 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// K-12 Grade Level Selector Bar (الصف الأول -> التوجيهي)
|
// Official Academic Grade Level Banner (Strict Grade-Gate — الصف العاشر فقط)
|
||||||
SizedBox(
|
Container(
|
||||||
height: 42,
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: ListView.builder(
|
decoration: BoxDecoration(
|
||||||
scrollDirection: Axis.horizontal,
|
color: AppColors.saqelCyan.withAlpha(20),
|
||||||
itemCount: (state.availableGrades.isNotEmpty ? state.availableGrades : GradeLevelModel.allK12Grades).length,
|
borderRadius: BorderRadius.circular(16),
|
||||||
itemBuilder: (context, gIdx) {
|
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
|
||||||
final grade = (state.availableGrades.isNotEmpty ? state.availableGrades : GradeLevelModel.allK12Grades)[gIdx];
|
),
|
||||||
final isSelected = state.selectedGrade == grade.key || state.selectedGrade.replaceAll('grade_', '') == grade.key.replaceAll('grade_', '');
|
child: Row(
|
||||||
|
children: [
|
||||||
return Padding(
|
Container(
|
||||||
padding: const EdgeInsets.only(left: 8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: InkWell(
|
decoration: BoxDecoration(
|
||||||
onTap: () => context.read<CurriculumCubit>().selectGrade(grade.key),
|
color: AppColors.saqelCyan.withAlpha(35),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(10),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
child: const Icon(CupertinoIcons.lock_shield_fill, color: AppColors.saqelCyan, size: 18),
|
||||||
},
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'الصف العاشر الأساسي — المسار الأكاديمي المعتمد',
|
||||||
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||||
|
),
|
||||||
|
SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'خطة دراسية موحدة ومطابقة للمنهاج الوزاري الأردني المطور',
|
||||||
|
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
|
||||||
import '../../../data/models/error_notebook_model.dart';
|
import '../../../data/models/error_notebook_model.dart';
|
||||||
import '../../../data/repositories/error_notebook_repository.dart';
|
import '../../../data/repositories/error_notebook_repository.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
|||||||
late AnimationController _watermarkController;
|
late AnimationController _watermarkController;
|
||||||
VideoPlayerController? _videoController;
|
VideoPlayerController? _videoController;
|
||||||
bool _isVideoInitialized = false;
|
bool _isVideoInitialized = false;
|
||||||
|
String? _videoInitError;
|
||||||
int _lastSyncedPosition = -1;
|
int _lastSyncedPosition = -1;
|
||||||
Timer? _controlsHideTimer;
|
Timer? _controlsHideTimer;
|
||||||
bool _showVideoControls = true;
|
bool _showVideoControls = true;
|
||||||
@@ -162,6 +163,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isVideoInitialized = true;
|
_isVideoInitialized = true;
|
||||||
|
_videoInitError = null;
|
||||||
});
|
});
|
||||||
_revealVideoControls();
|
_revealVideoControls();
|
||||||
if (state.currentPositionSeconds > 3) {
|
if (state.currentPositionSeconds > 3) {
|
||||||
@@ -178,6 +180,13 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
|||||||
_videoController!.play();
|
_videoController!.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}).catchError((err) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isVideoInitialized = false;
|
||||||
|
_videoInitError = 'الفيديو قيد المزامنة والتجهيز على خادم البث المباشر';
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (_isVideoInitialized) {
|
} else if (_isVideoInitialized) {
|
||||||
// Sync play/pause state
|
// Sync play/pause state
|
||||||
@@ -311,7 +320,31 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Center Lesson Title & Visuals
|
// Center Lesson Title & Visuals
|
||||||
if (_showVideoControls) Center(
|
if (_videoInitError != null)
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(CupertinoIcons.videocam_fill, color: AppColors.guardianAmber, size: 44),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_videoInitError!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'يمكنك دراسة نتاجات وملخص الدرس عبر تبويب الكتب والمذكرات',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (_showVideoControls) Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -340,10 +340,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -356,10 +356,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -585,10 +585,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.11"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ class SuperAdminTheme {
|
|||||||
static const Color surface = Color(0xFF0E1422);
|
static const Color surface = Color(0xFF0E1422);
|
||||||
static const Color surfaceCard = Color(0xFF141C2E);
|
static const Color surfaceCard = Color(0xFF141C2E);
|
||||||
static const Color border = Color(0xFF1E293B);
|
static const Color border = Color(0xFF1E293B);
|
||||||
|
|
||||||
|
static const Color backgroundDark = Color(0xFF06090F);
|
||||||
|
static const Color surfaceDark = Color(0xFF0E1422);
|
||||||
|
static const Color crimsonRed = Color(0xFFEF4444);
|
||||||
|
|
||||||
static const Color royalGold = Color(0xFFFFD166);
|
static const Color royalGold = Color(0xFFFFD166);
|
||||||
static const Color imperialPurple = Color(0xFF8B5CF6);
|
static const Color imperialPurple = Color(0xFF8B5CF6);
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ class _SuperAdminShellState extends State<SuperAdminShell> {
|
|||||||
label: 'الرصد الكلي',
|
label: 'الرصد الكلي',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(CupertinoIcons.cpu),
|
icon: Icon(CupertinoIcons.sparkles),
|
||||||
activeIcon: Icon(CupertinoIcons.cpu, color: SuperAdminTheme.royalGold),
|
activeIcon: Icon(CupertinoIcons.sparkles, color: SuperAdminTheme.royalGold),
|
||||||
label: 'الذكاء المحلي',
|
label: 'الذكاء المحلي',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class AiClusterTab extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.cpu, color: SuperAdminTheme.cyberCyan, size: 22),
|
Icon(CupertinoIcons.sparkles, color: SuperAdminTheme.cyberCyan, size: 22),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'عنقود الذكاء الاصطناعي السيادي (Local Inference)',
|
'عنقود الذكاء الاصطناعي السيادي (Local Inference)',
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class TreasuryCliqTab extends StatelessWidget {
|
|||||||
|
|
||||||
if (state is TreasuryLoaded) {
|
if (state is TreasuryLoaded) {
|
||||||
final pendingItems = state.queue.where((item) => item.status == 'queued').toList();
|
final pendingItems = state.queue.where((item) => item.status == 'queued').toList();
|
||||||
final completedItems = state.queue.where((item) => item.status == 'completed').toList();
|
|
||||||
final double pendingSum = pendingItems.fold(0.0, (acc, item) => acc + item.amountJod);
|
final double pendingSum = pendingItems.fold(0.0, (acc, item) => acc + item.amountJod);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
|||||||
@@ -316,10 +316,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -332,10 +332,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -561,10 +561,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.11"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ void main() {
|
|||||||
class SaqelTeacherApp extends StatelessWidget {
|
class SaqelTeacherApp extends StatelessWidget {
|
||||||
final TeacherRepository repository;
|
final TeacherRepository repository;
|
||||||
|
|
||||||
const SaqelTeacherApp({super.key, required this.repository});
|
SaqelTeacherApp({super.key, TeacherRepository? repository})
|
||||||
|
: repository = repository ?? TeacherRepository();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -316,10 +316,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -332,10 +332,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -561,10 +561,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.11"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:teacher_app/main.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Teacher App Smoke Test', (WidgetTester tester) async {
|
testWidgets('Teacher App Smoke Test', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const SaqelTeacherApp());
|
await tester.pumpWidget(SaqelTeacherApp());
|
||||||
expect(find.text('استوديو المعلم المعتمد — صَقِل'), findsOneWidget);
|
expect(find.text('استوديو المعلم المعتمد — صَقِل'), findsOneWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user