Implement verified curriculum, video review, watch sessions, ledger safeguards, and remove demo data

This commit is contained in:
Hamza-Ayed
2026-09-09 17:04:08 +03:00
parent 0dc5df7b5e
commit 5af684a892
49 changed files with 1809 additions and 3005 deletions
@@ -4,7 +4,6 @@ import '../../../core/network/api_client.dart';
import '../../../core/services/local_document_storage_service.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../../data/datasources/curriculum_baked_data.dart';
import '../../widgets/luxury_widgets.dart';
import '../../widgets/interactive_english_passage_view.dart';
import 'physics_interactive_lab_view.dart';
@@ -19,7 +18,7 @@ import 'english_interactive_lab_view.dart';
/// الهدف المعماري:
/// استعراض الكتب المدرسية المقررة والمذكرات الوزارية وأوراق العمل محلياً وسحابياً:
/// 1. دعم التخزين المحلي (Offline Storage): قراءة وتصفح دائم دون اتصال بعد الحفظ.
/// 2. محتوى وزاري مخبوز وأصيل (CurriculumBakedData) للمواد الثلاث (رياضيات 10، فيزياء 10، إنجليزي 10).
/// 2. محتوى وزاري منشور من الخادم أو نسخة محلية موثقة منه فقط.
/// 3. دعم الناطق الصوتي الذكي (English TTS) المدمج لقراءة النصوص والمفردات.
/// 4. ربط فوري بالمختبر التفاعلي (Virtual Lab) لمفاهيم المتجهات والفيزياء.
/// 5. التحكم بحجم الخط وزر الحفظ في الجهاز.
@@ -55,6 +54,7 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
bool _isSavedLocally = false;
String _documentContent = '';
List<DocumentSection> _sections = [];
String? _loadError;
bool get _isEnglish =>
widget.subjectTitle.contains('إنجليز') ||
@@ -126,20 +126,14 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
if (mounted) setState(() => _isLoading = false);
return;
}
} catch (_) {
// Graceful fallback to rich baked curriculum data
} catch (_) {}
if (mounted) {
setState(() {
_loadError = 'المستند المطلوب غير متاح حالياً. لا يمكن عرض بديل لدرس مختلف.';
_isLoading = false;
});
}
// 3. Fallback to authentic Jordanian curriculum baked content
final baked = CurriculumBakedData.getDocument(
subjectId: _effectiveSubjectId,
type: widget.documentType,
filePath: _effectiveFilePath,
title: widget.title,
);
_processContent(baked);
if (mounted) setState(() => _isLoading = false);
}
Future<void> _toggleSaveLocal() async {
@@ -266,7 +260,15 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
textDirection: _isEnglish ? TextDirection.ltr : TextDirection.rtl,
child: _isLoading
? const Center(child: CupertinoActivityIndicator(color: AppColors.saqelCyan))
: SingleChildScrollView(
: _loadError != null
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(_loadError!, textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.textSecondaryDark)),
),
)
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
File diff suppressed because it is too large Load Diff
@@ -29,8 +29,7 @@ import 'math_interactive_lab_view.dart';
import 'english_interactive_lab_view.dart';
import '../../../data/models/exam_model.dart';
import '../../../data/repositories/app_repositories.dart';
import '../../../core/services/local_document_storage_service.dart';
import '../../../data/datasources/curriculum_baked_data.dart';
import '../../../data/repositories/curriculum_repository.dart';
/// الشاشة المركزية للمادة الدراسية وبوابات الدروس والامتحانات والمصادر
class SubjectHubScreen extends StatefulWidget {
@@ -690,29 +689,10 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
onPressed: () {
Navigator.of(ctx).pop();
final docType = title.contains('كتاب') ? 'textbook' : 'worksheet';
final content = CurriculumBakedData.getDocument(
subjectId: widget.subject.id,
type: docType,
filePath: filePath,
title: title,
);
await LocalDocumentStorageService.saveDocumentLocally(
subjectId: widget.subject.id,
type: docType,
filePath: filePath ?? title,
title: title,
content: content,
);
if (context.mounted) {
SaqelToast.showSuccess(
context,
'تم حفظ المستند في الجهاز — متاح الآن للقراءة دون إنترنت 📖',
title: 'حفظ محلي مكتمل',
);
}
SaqelToast.showInfo(context,
'افتح المستند أولاً ثم احفظ النسخة المنشورة نفسها في الجهاز.');
},
icon: const Icon(CupertinoIcons.cloud_download, size: 18),
label: const Text('حفظ في الجهاز', style: TextStyle(fontWeight: FontWeight.w700)),
@@ -841,23 +821,44 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
);
}
/// Direct Launch of Socratic Video Player or Unavailable Notice
void _showLessonVideoSelector(BuildContext context, CurriculumLessonItemModel lesson) {
if (!lesson.hasVideo) {
/// Loads the server-authoritative list: one version opens directly; several
/// versions require an explicit teacher choice.
Future<void> _showLessonVideoSelector(BuildContext context, CurriculumLessonItemModel lesson) async {
final curriculumLessonId = lesson.curriculumLessonId;
if (!lesson.hasVideo || curriculumLessonId == null || curriculumLessonId.isEmpty) {
_showNoVideoAvailableSheet(context, lesson);
return;
}
// Direct authentic launch: zero mockups, zero fake teacher names
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => SocraticVideoPlayerScreen(
lesson: lesson,
subject: widget.subject,
instructorName: 'منصة صَقِل التعليمية المعتمدة',
videoSourceLabel: 'الشرح الرقمي الرسمي المعتمد',
),
),
);
try {
final videos = await CurriculumRepository().getPublishedLessonVideos(curriculumLessonId);
if (!context.mounted) return;
if (videos.isEmpty) { _showNoVideoAvailableSheet(context, lesson); return; }
PublishedLessonVideoModel? chosen;
if (videos.length == 1) {
chosen = videos.first;
} else {
chosen = await showCupertinoModalPopup<PublishedLessonVideoModel>(
context: context,
builder: (sheetContext) => CupertinoActionSheet(
title: const Text('اختر شرح المعلم'),
message: const Text('تُعرض الحصص المنشورة لهذا الدرس فقط، مرتبة بالتقييم الموثق.'),
actions: videos.map((video) => CupertinoActionSheetAction(
onPressed: () => Navigator.of(sheetContext).pop(video),
child: Text(video.ratingCount == 0 ? '${video.teacherName} — جديد' : '${video.teacherName} — ★ ${video.rating.toStringAsFixed(1)} (${video.ratingCount})'),
)).toList(),
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(sheetContext).pop(), child: const Text('إلغاء')),
),
);
}
if (chosen == null || !context.mounted) return;
Navigator.of(context).push(CupertinoPageRoute(builder: (_) => SocraticVideoPlayerScreen(
lesson: lesson, subject: widget.subject, instructorName: chosen!.teacherName,
videoSourceLabel: chosen.ratingCount == 0 ? 'حصة منشورة جديدة' : 'تقييم موثق: ${chosen.rating.toStringAsFixed(1)}',
selectedVideoVersionId: chosen.videoVersionId,
)));
} catch (e) {
if (context.mounted) SaqelToast.showError(context, 'تعذر تحميل الحصص المنشورة لهذا الدرس.');
}
}
}
@@ -35,6 +35,7 @@ class SocraticVideoPlayerScreen extends StatefulWidget {
final SubjectModel? subject;
final String? instructorName;
final String? videoSourceLabel;
final String? selectedVideoVersionId;
const SocraticVideoPlayerScreen({
super.key,
@@ -42,6 +43,7 @@ class SocraticVideoPlayerScreen extends StatefulWidget {
this.subject,
this.instructorName,
this.videoSourceLabel,
this.selectedVideoVersionId,
});
@override
@@ -127,7 +129,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
@override
void initState() {
super.initState();
context.read<VideoPlaybackCubit>().loadLesson(widget.lesson, subject: widget.subject);
context.read<VideoPlaybackCubit>().loadLesson(widget.lesson, subject: widget.subject, selectedVideoVersionId: widget.selectedVideoVersionId);
// Dynamic Floating Forensic Anti-Piracy Watermark Animation
_watermarkController = AnimationController(
@@ -156,6 +158,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
@override
void dispose() {
context.read<VideoPlaybackCubit>().endWatchSession();
_tts.stop();
_playbackTicker?.cancel();
_controlsHideTimer?.cancel();