Update Saqel Platform: 2026-09-13 16:31:17
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user