Implement verified curriculum, video review, watch sessions, ledger safeguards, and remove demo data
This commit is contained in:
@@ -71,6 +71,9 @@ class VideoPlaybackError extends VideoPlaybackState {
|
||||
class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
final CurriculumRepository _repo;
|
||||
int _lastObservedPosition = -1;
|
||||
String? _watchSessionId;
|
||||
int _watchSequence = 1;
|
||||
int _lastWatchEventPosition = 0;
|
||||
|
||||
VideoPlaybackCubit({CurriculumRepository? repo})
|
||||
: _repo = repo ?? CurriculumRepository(),
|
||||
@@ -86,19 +89,30 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
return fallbackId ?? 'lesson_default';
|
||||
}
|
||||
|
||||
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async {
|
||||
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject, String? selectedVideoVersionId}) async {
|
||||
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
|
||||
_lastObservedPosition = -1;
|
||||
emit(VideoPlaybackLoading());
|
||||
final storageKey = _getLessonStorageKey(lesson);
|
||||
|
||||
try {
|
||||
final cleanKey = (lesson.markdownFilePath ?? lesson.id).replaceAll(RegExp(r'\.md$'), '');
|
||||
var playback = await _repo.getLessonPlayback(cleanKey, subjectId: subject?.id, title: lesson.title);
|
||||
final versionId = lesson.curriculumLessonId;
|
||||
if (versionId == null || versionId.isEmpty) {
|
||||
throw StateError('هذا الدرس غير منشور بعد ضمن حزمة المحتوى المعتمدة.');
|
||||
}
|
||||
final published = await _repo.getPublishedLessonVideos(versionId);
|
||||
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);
|
||||
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 (_) {}
|
||||
}
|
||||
|
||||
// Load saved resume position strictly per video
|
||||
int resumePos = playback.lastPositionSeconds ?? 0;
|
||||
Set<int> passedIds = {};
|
||||
@@ -151,6 +165,13 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> endWatchSession() async {
|
||||
final session = _watchSessionId;
|
||||
if (session == null) return;
|
||||
_watchSessionId = null;
|
||||
try { await _repo.recordWatchEvent(session, ++_watchSequence, 'end', _lastObservedPosition.clamp(0, 1 << 31) * 1000); } catch (_) {}
|
||||
}
|
||||
|
||||
void togglePlayPause() {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
|
||||
@@ -197,11 +218,11 @@ 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);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.log('Progress sync deferred: $e', tag: 'VIDEO_CUBIT');
|
||||
|
||||
Reference in New Issue
Block a user