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:
Hamza-Ayed
2026-09-04 23:41:32 +03:00
parent ae00aa943e
commit e7bc20bc98
22 changed files with 291 additions and 316 deletions
@@ -76,19 +76,34 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
: _repo = repo ?? CurriculumRepository(),
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 {
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
_lastObservedPosition = -1;
emit(VideoPlaybackLoading());
final storageKey = _getLessonStorageKey(lesson);
try {
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;
Set<int> passedIds = {};
try {
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;
final savedPassed = prefs.getStringList('passed_checkpoints_$storageKey') ?? [];
passedIds = savedPassed.map((s) => int.tryParse(s) ?? 0).where((id) => id > 0).toSet();
} catch (_) {}
emit(VideoPlaybackReady(
@@ -96,18 +111,22 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
lessonItem: lesson,
subject: subject,
currentPositionSeconds: resumePos,
passedCheckpointIds: passedIds,
isPlaying: true,
));
} catch (e) {
AppLogger.log('Playback API unavailable (${lesson.title}): $e — Switching to offline Socratic fallback', tag: 'VIDEO_CUBIT');
final fallback = _buildResilientLessonPlayback(lesson, subject: subject);
// Load saved resume position
// Load saved resume position strictly per video
int resumePos = fallback.lastPositionSeconds ?? 0;
Set<int> passedIds = {};
try {
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;
final savedPassed = prefs.getStringList('passed_checkpoints_$storageKey') ?? [];
passedIds = savedPassed.map((s) => int.tryParse(s) ?? 0).where((id) => id > 0).toSet();
} catch (_) {}
emit(VideoPlaybackReady(
@@ -115,6 +134,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
lessonItem: lesson,
subject: subject,
currentPositionSeconds: resumePos,
passedCheckpointIds: passedIds,
isPlaying: true,
));
}
@@ -197,21 +217,12 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
LessonVersionModel(
lessonId: 101,
isAi: true,
teacherName: 'منصة صَقِل الذكية 🤖',
teacherName: 'منصة صَقِل التعليمية المعتمدة',
schoolName: 'المركز الرقمي المعتمد',
label: 'شرح الذكاء الاصطناعي الرسمي',
label: 'الشرح الرقمي الرسمي المعتمد',
isRecommended: true,
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;
if (currentState is! VideoPlaybackReady) return;
try {
final storageKey = _getLessonStorageKey(currentState.lessonItem, fallbackId: '${currentState.playbackData.lessonId}');
final prefs = await SharedPreferences.getInstance();
if (currentState.lessonItem != null) {
await prefs.setInt('saved_video_pos_${currentState.lessonItem!.id}', positionSeconds);
await prefs.setInt('saved_video_pos_$storageKey', positionSeconds);
if (currentState.passedCheckpointIds.isNotEmpty) {
await prefs.setStringList(
'passed_checkpoints_$storageKey',
currentState.passedCheckpointIds.map((id) => id.toString()).toList(),
);
}
if (currentState.playbackData.lessonId > 0) {
await prefs.setInt('saved_video_pos_${currentState.playbackData.lessonId}', positionSeconds);
await _repo.saveProgress(
lessonId: currentState.playbackData.lessonId,
positionSeconds: positionSeconds,
@@ -307,6 +322,13 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE');
final updatedPassed = Set<int>.from(currentState.passedCheckpointIds)..add(cp.id);
_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(
clearActiveCheckpoint: true,
isPlaying: true,