Update Saqel Platform: 2026-09-13 16:31:17

This commit is contained in:
Hamza-Ayed
2026-09-13 16:31:18 +03:00
parent 81fa8c5dc8
commit 72c6575765
25 changed files with 6997 additions and 3852 deletions
@@ -91,7 +91,13 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
}
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject, String? selectedVideoVersionId}) async {
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
AppLogger.event('LoadLessonPlaybackRequested', details: {
'lessonTitle': lesson.title,
'lessonId': lesson.id,
'curriculumLessonId': lesson.curriculumLessonId,
'selectedVersionId': selectedVideoVersionId,
'hasVideo': lesson.hasVideo,
}, tag: 'VIDEO_CUBIT');
_lastObservedPosition = -1;
emit(VideoPlaybackLoading());
final storageKey = _getLessonStorageKey(lesson);
@@ -102,16 +108,31 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
throw StateError('هذا الدرس غير منشور بعد ضمن حزمة المحتوى المعتمدة.');
}
final published = await _repo.getPublishedLessonVideos(versionId);
AppLogger.event('PublishedVideosLoaded', details: {
'count': published.length,
'teachers': published.map((p) => '${p.teacherName} (⭐ ${p.rating.toStringAsFixed(1)})').toList(),
}, tag: 'VIDEO_CUBIT');
if (published.isEmpty) throw StateError('لا توجد حصة منشورة ومصرح بها لهذا الدرس بعد.');
// Selection is performed in the lesson screen when several teachers exist.
var playback = await _repo.getVideoVersionPlayback(selectedVideoVersionId ?? published.first.videoVersionId);
final targetVersionId = selectedVideoVersionId ?? published.first.videoVersionId;
var playback = await _repo.getVideoVersionPlayback(targetVersionId);
AppLogger.event('PlaybackDataLoaded', details: {
'versionId': playback.videoVersionId,
'videoUrl': playback.videoUrl,
'durationSeconds': playback.durationSeconds,
'checkpointsCount': playback.checkpoints.length,
}, tag: 'VIDEO_CUBIT');
if (playback.videoUrl.isEmpty) {
throw StateError('لم يربط الخادم فيديو R2 بهذا الدرس بعد.');
}
_watchSessionId = null; _watchSequence = 1; _lastWatchEventPosition = 0;
if (playback.videoVersionId != null && playback.videoVersionId!.isNotEmpty) {
try { _watchSessionId = await _repo.startWatchSession(playback.videoVersionId!); } catch (_) {}
try {
_watchSessionId = await _repo.startWatchSession(playback.videoVersionId!);
AppLogger.event('WatchSessionStarted', details: {'sessionId': _watchSessionId}, tag: 'VIDEO_CUBIT');
} catch (_) {}
}
// Load saved resume position strictly per video
@@ -125,6 +146,11 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
passedIds = savedPassed.map((s) => int.tryParse(s) ?? 0).where((id) => id > 0).toSet();
} catch (_) {}
AppLogger.event('VideoPlaybackStateEmitted', details: {
'resumePosition': resumePos,
'passedCheckpointsCount': passedIds.length,
}, tag: 'VIDEO_CUBIT');
emit(VideoPlaybackReady(
playbackData: playback,
lessonItem: lesson,
@@ -176,13 +202,16 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
void togglePlayPause() {
final currentState = state;
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
emit(currentState.copyWith(isPlaying: !currentState.isPlaying));
final nextState = !currentState.isPlaying;
AppLogger.event('VideoPlayPauseToggled', details: {'isPlaying': nextState}, tag: 'VIDEO_CUBIT');
emit(currentState.copyWith(isPlaying: nextState));
}
}
void pause() {
final currentState = state;
if (currentState is VideoPlaybackReady && currentState.isPlaying) {
AppLogger.event('VideoPaused', tag: 'VIDEO_CUBIT');
emit(currentState.copyWith(isPlaying: false));
}
}
@@ -190,6 +219,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
void play() {
final currentState = state;
if (currentState is VideoPlaybackReady && !currentState.isPlaying && currentState.activeCheckpoint == null) {
AppLogger.event('VideoResumed', tag: 'VIDEO_CUBIT');
emit(currentState.copyWith(isPlaying: true));
}
}
@@ -211,6 +241,13 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
final actualSeek = blockingCheckpoint != null ? blockingCheckpoint.timestampSeconds : seconds;
_lastObservedPosition = actualSeek;
AppLogger.event('VideoSeekExecuted', details: {
'targetSeconds': seconds,
'actualSeek': actualSeek,
'blockedByCheckpoint': blockingCheckpoint != null,
'checkpointQuestion': blockingCheckpoint?.questionText,
}, tag: 'VIDEO_CUBIT');
emit(currentState.copyWith(
currentPositionSeconds: actualSeek,
activeCheckpoint: blockingCheckpoint,
@@ -252,7 +289,12 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
if (selectedOption.isCorrect) {
// Correct Answer -> Reward readiness score (+0.5%) & resume video
AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE');
AppLogger.event('SocraticAnswerCorrect', details: {
'checkpointId': cp.id,
'question': cp.questionText,
'selected': selectedOption.text,
'bonus': 0.5,
}, tag: 'SOCRATIC_ENGINE');
final updatedPassed = Set<int>.from(currentState.passedCheckpointIds)..add(cp.id);
_lastObservedPosition = cp.timestampSeconds;
@@ -279,7 +321,13 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
// Wrong Answer -> Socratic Productive Struggle: Rewind video by N seconds from checkpoint
final rewindTo = (cp.timestampSeconds - cp.rewindSecondsOnFail).clamp(0, currentState.playbackData.durationSeconds);
_lastObservedPosition = rewindTo;
AppLogger.log('❌ Incorrect answer. Socratic remediation: Rewinding to ${rewindTo}s.', tag: 'SOCRATIC_ENGINE');
AppLogger.event('SocraticAnswerIncorrect', details: {
'checkpointId': cp.id,
'question': cp.questionText,
'selected': selectedOption.text,
'rewindSeconds': cp.rewindSecondsOnFail,
'rewindTo': rewindTo,
}, tag: 'SOCRATIC_ENGINE');
emit(currentState.copyWith(
clearActiveCheckpoint: true,
currentPositionSeconds: rewindTo,
@@ -290,7 +338,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
// Auto-record gap to Smart Error Notebook
final correctOpt = cp.options.firstWhere(
(o) => o.isCorrect,
orElse: () => SocraticOptionModel(id: 0, text: '', isCorrect: false),
orElse: () => const SocraticOptionModel(id: 0, text: '', isCorrect: false),
);
ErrorNotebookRepository().logError(
subjectId: currentState.subject?.id ?? 'physics_10',