Harden published curriculum and student flows
This commit is contained in:
@@ -9,6 +9,7 @@ import '../../data/repositories/error_notebook_repository.dart';
|
||||
abstract class VideoPlaybackState {}
|
||||
|
||||
class VideoPlaybackInitial extends VideoPlaybackState {}
|
||||
|
||||
class VideoPlaybackLoading extends VideoPlaybackState {}
|
||||
|
||||
class VideoPlaybackReady extends VideoPlaybackState {
|
||||
@@ -53,9 +54,12 @@ class VideoPlaybackReady extends VideoPlaybackState {
|
||||
playbackData: playbackData ?? this.playbackData,
|
||||
lessonItem: lessonItem ?? this.lessonItem,
|
||||
subject: subject ?? this.subject,
|
||||
currentPositionSeconds: currentPositionSeconds ?? this.currentPositionSeconds,
|
||||
currentPositionSeconds:
|
||||
currentPositionSeconds ?? this.currentPositionSeconds,
|
||||
isPlaying: isPlaying ?? this.isPlaying,
|
||||
activeCheckpoint: clearActiveCheckpoint ? null : (activeCheckpoint ?? this.activeCheckpoint),
|
||||
activeCheckpoint: clearActiveCheckpoint
|
||||
? null
|
||||
: (activeCheckpoint ?? this.activeCheckpoint),
|
||||
isCheckpointPassed: isCheckpointPassed ?? this.isCheckpointPassed,
|
||||
remediationNotice: remediationNotice,
|
||||
readinessBonusAdded: readinessBonusAdded ?? this.readinessBonusAdded,
|
||||
@@ -80,8 +84,10 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
: _repo = repo ?? CurriculumRepository(),
|
||||
super(VideoPlaybackInitial());
|
||||
|
||||
String _getLessonStorageKey(CurriculumLessonItemModel? lesson, {String? fallbackId}) {
|
||||
if (lesson?.markdownFilePath != null && lesson!.markdownFilePath!.isNotEmpty) {
|
||||
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) {
|
||||
@@ -90,14 +96,17 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
return fallbackId ?? 'lesson_default';
|
||||
}
|
||||
|
||||
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject, String? selectedVideoVersionId}) async {
|
||||
AppLogger.event('LoadLessonPlaybackRequested', details: {
|
||||
'lessonTitle': lesson.title,
|
||||
'lessonId': lesson.id,
|
||||
'curriculumLessonId': lesson.curriculumLessonId,
|
||||
'selectedVersionId': selectedVideoVersionId,
|
||||
'hasVideo': lesson.hasVideo,
|
||||
}, tag: 'VIDEO_CUBIT');
|
||||
Future<void> loadLesson(CurriculumLessonItemModel lesson,
|
||||
{SubjectModel? subject, String? selectedVideoVersionId}) async {
|
||||
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);
|
||||
@@ -108,32 +117,38 @@ 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('لا توجد حصة منشورة ومصرح بها لهذا الدرس بعد.');
|
||||
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('لا توجد حصة منشورة ومصرح بها لهذا الدرس بعد.');
|
||||
|
||||
final targetVersionId = 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');
|
||||
AppLogger.event('PlaybackDataLoaded',
|
||||
details: {
|
||||
'versionId': playback.videoVersionId,
|
||||
'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!);
|
||||
AppLogger.event('WatchSessionStarted', details: {'sessionId': _watchSessionId}, tag: 'VIDEO_CUBIT');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// A watch session represents actual playback. The screen starts it only after
|
||||
// the exact published video URL initializes successfully.
|
||||
_watchSessionId = null;
|
||||
_watchSequence = 1;
|
||||
_lastWatchEventPosition = 0;
|
||||
|
||||
// Load saved resume position strictly per video
|
||||
int resumePos = playback.lastPositionSeconds ?? 0;
|
||||
@@ -142,14 +157,20 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
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();
|
||||
final savedPassed =
|
||||
prefs.getStringList('passed_checkpoints_$storageKey') ?? [];
|
||||
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');
|
||||
AppLogger.event('VideoPlaybackStateEmitted',
|
||||
details: {
|
||||
'resumePosition': resumePos,
|
||||
'passedCheckpointsCount': passedIds.length,
|
||||
},
|
||||
tag: 'VIDEO_CUBIT');
|
||||
|
||||
emit(VideoPlaybackReady(
|
||||
playbackData: playback,
|
||||
@@ -157,26 +178,33 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
subject: subject,
|
||||
currentPositionSeconds: resumePos,
|
||||
passedCheckpointIds: passedIds,
|
||||
isPlaying: true,
|
||||
// Playback becomes active only after the screen initializes the actual stream.
|
||||
isPlaying: false,
|
||||
));
|
||||
} catch (e) {
|
||||
AppLogger.error('Playback API unavailable (${lesson.title})', error: e, tag: 'VIDEO_CUBIT');
|
||||
AppLogger.error('Playback API unavailable (${lesson.title})',
|
||||
error: e, tag: 'VIDEO_CUBIT');
|
||||
emit(VideoPlaybackError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void updatePosition(int seconds) {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
|
||||
if (currentState is VideoPlaybackReady &&
|
||||
currentState.activeCheckpoint == null) {
|
||||
// Check if natural forward playback reaches a Socratic checkpoint
|
||||
for (var cp in currentState.playbackData.checkpoints) {
|
||||
final hitCheckpoint = !currentState.passedCheckpointIds.contains(cp.id) &&
|
||||
_lastObservedPosition < cp.timestampSeconds &&
|
||||
seconds >= cp.timestampSeconds &&
|
||||
(seconds - _lastObservedPosition).abs() <= 3; // Natural sequential playback
|
||||
final hitCheckpoint =
|
||||
!currentState.passedCheckpointIds.contains(cp.id) &&
|
||||
_lastObservedPosition < cp.timestampSeconds &&
|
||||
seconds >= cp.timestampSeconds &&
|
||||
(seconds - _lastObservedPosition).abs() <=
|
||||
3; // Natural sequential playback
|
||||
|
||||
if (hitCheckpoint) {
|
||||
AppLogger.log('🚨 Socratic Checkpoint Triggered at ${cp.timestampSeconds}s! (${cp.questionText})', tag: 'SOCRATIC_ENGINE');
|
||||
AppLogger.log(
|
||||
'🚨 Socratic Checkpoint Triggered at ${cp.timestampSeconds}s! (${cp.questionText})',
|
||||
tag: 'SOCRATIC_ENGINE');
|
||||
_lastObservedPosition = cp.timestampSeconds;
|
||||
emit(currentState.copyWith(
|
||||
currentPositionSeconds: cp.timestampSeconds,
|
||||
@@ -196,14 +224,43 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
final session = _watchSessionId;
|
||||
if (session == null) return;
|
||||
_watchSessionId = null;
|
||||
try { await _repo.recordWatchEvent(session, ++_watchSequence, 'end', _lastObservedPosition.clamp(0, 1 << 31) * 1000); } catch (_) {}
|
||||
try {
|
||||
await _repo.recordWatchEvent(session, ++_watchSequence, 'end',
|
||||
_lastObservedPosition.clamp(0, 1 << 31) * 1000);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> startWatchSessionAfterPlaybackReady() async {
|
||||
final currentState = state;
|
||||
final versionId = currentState is VideoPlaybackReady
|
||||
? currentState.playbackData.videoVersionId
|
||||
: null;
|
||||
if (_watchSessionId != null || versionId == null || versionId.isEmpty)
|
||||
return;
|
||||
try {
|
||||
_watchSequence = 1;
|
||||
_lastWatchEventPosition = 0;
|
||||
_watchSessionId = await _repo.startWatchSession(versionId);
|
||||
AppLogger.event('WatchSessionStartedAfterVideoReady',
|
||||
details: {
|
||||
'sessionId': _watchSessionId,
|
||||
'videoVersionId': versionId,
|
||||
},
|
||||
tag: 'VIDEO_CUBIT');
|
||||
} catch (e) {
|
||||
AppLogger.log(
|
||||
'Watch session unavailable after playback initialization: $e',
|
||||
tag: 'VIDEO_CUBIT');
|
||||
}
|
||||
}
|
||||
|
||||
void togglePlayPause() {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
|
||||
if (currentState is VideoPlaybackReady &&
|
||||
currentState.activeCheckpoint == null) {
|
||||
final nextState = !currentState.isPlaying;
|
||||
AppLogger.event('VideoPlayPauseToggled', details: {'isPlaying': nextState}, tag: 'VIDEO_CUBIT');
|
||||
AppLogger.event('VideoPlayPauseToggled',
|
||||
details: {'isPlaying': nextState}, tag: 'VIDEO_CUBIT');
|
||||
emit(currentState.copyWith(isPlaying: nextState));
|
||||
}
|
||||
}
|
||||
@@ -218,7 +275,9 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
|
||||
void play() {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && !currentState.isPlaying && currentState.activeCheckpoint == null) {
|
||||
if (currentState is VideoPlaybackReady &&
|
||||
!currentState.isPlaying &&
|
||||
currentState.activeCheckpoint == null) {
|
||||
AppLogger.event('VideoResumed', tag: 'VIDEO_CUBIT');
|
||||
emit(currentState.copyWith(isPlaying: true));
|
||||
}
|
||||
@@ -230,23 +289,29 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
// Find the earliest unpassed checkpoint before or at this target
|
||||
SocraticCheckpointModel? blockingCheckpoint;
|
||||
for (var cp in currentState.playbackData.checkpoints) {
|
||||
if (!currentState.passedCheckpointIds.contains(cp.id) && seconds > cp.timestampSeconds) {
|
||||
if (blockingCheckpoint == null || cp.timestampSeconds < blockingCheckpoint.timestampSeconds) {
|
||||
if (!currentState.passedCheckpointIds.contains(cp.id) &&
|
||||
seconds > cp.timestampSeconds) {
|
||||
if (blockingCheckpoint == null ||
|
||||
cp.timestampSeconds < blockingCheckpoint.timestampSeconds) {
|
||||
blockingCheckpoint = cp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If user tries to skip past an unpassed checkpoint, snap precisely to that checkpoint
|
||||
final actualSeek = blockingCheckpoint != null ? blockingCheckpoint.timestampSeconds : seconds;
|
||||
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');
|
||||
AppLogger.event('VideoSeekExecuted',
|
||||
details: {
|
||||
'targetSeconds': seconds,
|
||||
'actualSeek': actualSeek,
|
||||
'blockedByCheckpoint': blockingCheckpoint != null,
|
||||
'checkpointQuestion': blockingCheckpoint?.questionText,
|
||||
},
|
||||
tag: 'VIDEO_CUBIT');
|
||||
|
||||
emit(currentState.copyWith(
|
||||
currentPositionSeconds: actualSeek,
|
||||
@@ -256,11 +321,13 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveProgress({required int positionSeconds, required int watchedSeconds}) async {
|
||||
Future<void> saveProgress(
|
||||
{required int positionSeconds, required int watchedSeconds}) async {
|
||||
final currentState = state;
|
||||
if (currentState is! VideoPlaybackReady) return;
|
||||
try {
|
||||
final storageKey = _getLessonStorageKey(currentState.lessonItem, fallbackId: '${currentState.playbackData.lessonId}');
|
||||
final storageKey = _getLessonStorageKey(currentState.lessonItem,
|
||||
fallbackId: '${currentState.playbackData.lessonId}');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('saved_video_pos_$storageKey', positionSeconds);
|
||||
if (currentState.passedCheckpointIds.isNotEmpty) {
|
||||
@@ -270,11 +337,17 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
);
|
||||
}
|
||||
if (currentState.playbackData.lessonId > 0) {
|
||||
await _repo.saveProgress(lessonId: currentState.playbackData.lessonId, positionSeconds: positionSeconds, watchedSeconds: watchedSeconds);
|
||||
await _repo.saveProgress(
|
||||
lessonId: currentState.playbackData.lessonId,
|
||||
positionSeconds: positionSeconds,
|
||||
watchedSeconds: watchedSeconds);
|
||||
}
|
||||
if (_watchSessionId != null && positionSeconds - _lastWatchEventPosition >= 30) {
|
||||
_watchSequence++; _lastWatchEventPosition = positionSeconds;
|
||||
await _repo.recordWatchEvent(_watchSessionId!, _watchSequence, 'heartbeat', positionSeconds * 1000);
|
||||
if (_watchSessionId != null &&
|
||||
positionSeconds - _lastWatchEventPosition >= 30) {
|
||||
_watchSequence++;
|
||||
_lastWatchEventPosition = positionSeconds;
|
||||
await _repo.recordWatchEvent(_watchSessionId!, _watchSequence,
|
||||
'heartbeat', positionSeconds * 1000);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.log('Progress sync deferred: $e', tag: 'VIDEO_CUBIT');
|
||||
@@ -284,24 +357,30 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
/// Submit Socratic Checkpoint Answer
|
||||
bool submitCheckpointAnswer(SocraticOptionModel selectedOption) {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint != null) {
|
||||
if (currentState is VideoPlaybackReady &&
|
||||
currentState.activeCheckpoint != null) {
|
||||
final cp = currentState.activeCheckpoint!;
|
||||
|
||||
|
||||
if (selectedOption.isCorrect) {
|
||||
// Correct Answer -> Reward readiness score (+0.5%) & resume video
|
||||
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);
|
||||
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;
|
||||
|
||||
// Persist passed checkpoint for this specific video
|
||||
final storageKey = _getLessonStorageKey(currentState.lessonItem, fallbackId: '${currentState.playbackData.lessonId}');
|
||||
final storageKey = _getLessonStorageKey(currentState.lessonItem,
|
||||
fallbackId: '${currentState.playbackData.lessonId}');
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setStringList('passed_checkpoints_$storageKey', updatedPassed.map((id) => id.toString()).toList());
|
||||
prefs.setStringList('passed_checkpoints_$storageKey',
|
||||
updatedPassed.map((id) => id.toString()).toList());
|
||||
}).catchError((_) {});
|
||||
|
||||
emit(currentState.copyWith(
|
||||
@@ -309,36 +388,47 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
isPlaying: true,
|
||||
readinessBonusAdded: currentState.readinessBonusAdded + 0.5,
|
||||
passedCheckpointIds: updatedPassed,
|
||||
remediationNotice: 'إجابة نموذجية ممتازة! تم تعزيز مؤشر الجاهزية (+0.5%) 🚀',
|
||||
remediationNotice:
|
||||
'إجابة نموذجية ممتازة! تم تعزيز مؤشر الجاهزية (+0.5%) 🚀',
|
||||
));
|
||||
if (cp.id > 0) {
|
||||
_repo.submitCheckpoint(examId: cp.id, questionId: cp.questionId, optionId: selectedOption.id).catchError((e) {
|
||||
_repo
|
||||
.submitCheckpoint(
|
||||
examId: cp.id,
|
||||
questionId: cp.questionId,
|
||||
optionId: selectedOption.id)
|
||||
.catchError((e) {
|
||||
AppLogger.log('Checkpoint sync deferred: $e', tag: 'VIDEO_CUBIT');
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// Wrong Answer -> Socratic Productive Struggle: Rewind video by N seconds from checkpoint
|
||||
final rewindTo = (cp.timestampSeconds - cp.rewindSecondsOnFail).clamp(0, currentState.playbackData.durationSeconds);
|
||||
final rewindTo = (cp.timestampSeconds - cp.rewindSecondsOnFail)
|
||||
.clamp(0, currentState.playbackData.durationSeconds);
|
||||
_lastObservedPosition = rewindTo;
|
||||
AppLogger.event('SocraticAnswerIncorrect', details: {
|
||||
'checkpointId': cp.id,
|
||||
'question': cp.questionText,
|
||||
'selected': selectedOption.text,
|
||||
'rewindSeconds': cp.rewindSecondsOnFail,
|
||||
'rewindTo': rewindTo,
|
||||
}, 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,
|
||||
isPlaying: true,
|
||||
remediationNotice: 'تعثرت في هذا المفهوم. تم إرجاع الفيديو ${cp.rewindSecondsOnFail} ثانية لإعادة الاستماع بتركيز 🔄',
|
||||
remediationNotice:
|
||||
'تعثرت في هذا المفهوم. تم إرجاع الفيديو ${cp.rewindSecondsOnFail} ثانية لإعادة الاستماع بتركيز 🔄',
|
||||
));
|
||||
|
||||
// Auto-record gap to Smart Error Notebook
|
||||
final correctOpt = cp.options.firstWhere(
|
||||
(o) => o.isCorrect,
|
||||
orElse: () => const SocraticOptionModel(id: 0, text: '', isCorrect: false),
|
||||
orElse: () =>
|
||||
const SocraticOptionModel(id: 0, text: '', isCorrect: false),
|
||||
);
|
||||
ErrorNotebookRepository().logError(
|
||||
subjectId: currentState.subject?.id ?? 'physics_10',
|
||||
@@ -350,12 +440,19 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
correctAnswer: correctOpt.text,
|
||||
hint: cp.hint,
|
||||
errorCategory: 'conceptual',
|
||||
lessonId: currentState.playbackData.lessonId > 0 ? currentState.playbackData.lessonId : null,
|
||||
lessonId: currentState.playbackData.lessonId > 0
|
||||
? currentState.playbackData.lessonId
|
||||
: null,
|
||||
options: cp.options.map((o) => o.text).toList(),
|
||||
);
|
||||
|
||||
if (cp.id > 0 && cp.questionId > 0) {
|
||||
_repo.submitCheckpoint(examId: cp.id, questionId: cp.questionId, optionId: selectedOption.id).catchError((e) {
|
||||
_repo
|
||||
.submitCheckpoint(
|
||||
examId: cp.id,
|
||||
questionId: cp.questionId,
|
||||
optionId: selectedOption.id)
|
||||
.catchError((e) {
|
||||
AppLogger.log('Checkpoint sync deferred: $e', tag: 'VIDEO_CUBIT');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user