Implement verified curriculum, video review, watch sessions, ledger safeguards, and remove demo data
This commit is contained in:
+18
-16
@@ -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,
|
||||
|
||||
+23
-2217
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();
|
||||
|
||||
Reference in New Issue
Block a user