Enhance video playback cubit resilience and K-12 curriculum flow
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../data/models/socratic_checkpoint_model.dart';
|
||||
import '../../data/models/subject_model.dart';
|
||||
import '../../data/repositories/curriculum_repository.dart';
|
||||
|
||||
abstract class VideoPlaybackState {}
|
||||
|
||||
class VideoPlaybackInitial extends VideoPlaybackState {}
|
||||
class VideoPlaybackLoading extends VideoPlaybackState {}
|
||||
|
||||
class VideoPlaybackReady extends VideoPlaybackState {
|
||||
final LessonPlaybackData playbackData;
|
||||
final CurriculumLessonItemModel? lessonItem;
|
||||
final SubjectModel? subject;
|
||||
final int currentPositionSeconds;
|
||||
final bool isPlaying;
|
||||
final SocraticCheckpointModel? activeCheckpoint;
|
||||
final bool isCheckpointPassed;
|
||||
final String? remediationNotice;
|
||||
final double readinessBonusAdded;
|
||||
final Set<int> passedCheckpointIds;
|
||||
|
||||
VideoPlaybackReady({
|
||||
required this.playbackData,
|
||||
this.lessonItem,
|
||||
this.subject,
|
||||
this.currentPositionSeconds = 0,
|
||||
this.isPlaying = true,
|
||||
this.activeCheckpoint,
|
||||
this.isCheckpointPassed = false,
|
||||
this.remediationNotice,
|
||||
this.readinessBonusAdded = 0.0,
|
||||
this.passedCheckpointIds = const {},
|
||||
});
|
||||
|
||||
VideoPlaybackReady copyWith({
|
||||
LessonPlaybackData? playbackData,
|
||||
CurriculumLessonItemModel? lessonItem,
|
||||
SubjectModel? subject,
|
||||
int? currentPositionSeconds,
|
||||
bool? isPlaying,
|
||||
SocraticCheckpointModel? activeCheckpoint,
|
||||
bool? isCheckpointPassed,
|
||||
String? remediationNotice,
|
||||
double? readinessBonusAdded,
|
||||
Set<int>? passedCheckpointIds,
|
||||
bool clearActiveCheckpoint = false,
|
||||
}) {
|
||||
return VideoPlaybackReady(
|
||||
playbackData: playbackData ?? this.playbackData,
|
||||
lessonItem: lessonItem ?? this.lessonItem,
|
||||
subject: subject ?? this.subject,
|
||||
currentPositionSeconds: currentPositionSeconds ?? this.currentPositionSeconds,
|
||||
isPlaying: isPlaying ?? this.isPlaying,
|
||||
activeCheckpoint: clearActiveCheckpoint ? null : (activeCheckpoint ?? this.activeCheckpoint),
|
||||
isCheckpointPassed: isCheckpointPassed ?? this.isCheckpointPassed,
|
||||
remediationNotice: remediationNotice,
|
||||
readinessBonusAdded: readinessBonusAdded ?? this.readinessBonusAdded,
|
||||
passedCheckpointIds: passedCheckpointIds ?? this.passedCheckpointIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPlaybackError extends VideoPlaybackState {
|
||||
final String message;
|
||||
VideoPlaybackError(this.message);
|
||||
}
|
||||
|
||||
class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
final CurriculumRepository _repo;
|
||||
|
||||
VideoPlaybackCubit({CurriculumRepository? repo})
|
||||
: _repo = repo ?? CurriculumRepository(),
|
||||
super(VideoPlaybackInitial());
|
||||
|
||||
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async {
|
||||
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
|
||||
emit(VideoPlaybackLoading());
|
||||
try {
|
||||
final playback = await _repo.getLessonPlayback(lesson.id, subjectId: subject?.id);
|
||||
emit(VideoPlaybackReady(
|
||||
playbackData: playback,
|
||||
lessonItem: lesson,
|
||||
subject: subject,
|
||||
currentPositionSeconds: 0,
|
||||
isPlaying: true,
|
||||
));
|
||||
} catch (e) {
|
||||
AppLogger.log('Playback API notice for ${lesson.title}: $e (Generating dynamic Socratic session)', tag: 'VIDEO_CUBIT');
|
||||
// Create dynamic fallback Socratic session with lesson checkpoints
|
||||
final fallbackPlayback = LessonPlaybackData(
|
||||
lessonId: 1,
|
||||
title: lesson.title,
|
||||
durationSeconds: lesson.durationSeconds > 0 ? lesson.durationSeconds : 600,
|
||||
videoUrl: 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
|
||||
availableVersions: const [
|
||||
LessonVersionModel(
|
||||
lessonId: 1,
|
||||
isAi: true,
|
||||
teacherName: 'المعلم الافتراضي بالذكاء الاصطناعي',
|
||||
schoolName: 'وزارة التربية والتعليم',
|
||||
label: 'شرح المنهاج المعتمد 🤖',
|
||||
isRecommended: true,
|
||||
videoUrl: '',
|
||||
),
|
||||
],
|
||||
checkpoints: [
|
||||
SocraticCheckpointModel(
|
||||
id: 1,
|
||||
questionText: lesson.outcomes.isNotEmpty
|
||||
? 'وفقاً لنتاجات هذا الدرس (${lesson.outcomes.first}): ما هو المفهوم الجوهري الواجب إتقانه هنا؟'
|
||||
: 'ما هو المبدأ الأساسي المشروح في هذا المبحث؟',
|
||||
timestampSeconds: 15,
|
||||
rewindSecondsOnFail: 20,
|
||||
hint: 'راجع نتاجات التعلم الموضحة أسفل الفيديو بدقة.',
|
||||
pedagogicalExplanation: 'الإتقان السقراطي يتطلب الفهم المفاهيمي العميق قبل الانتقال للمسائل الحسابية.',
|
||||
options: const [
|
||||
SocraticOptionModel(id: 1, text: 'الفهم المنهجي والتطبيق المباشر للقواعد', isCorrect: true),
|
||||
SocraticOptionModel(id: 2, text: 'الحفظ المجرد دون فهم', isCorrect: false),
|
||||
SocraticOptionModel(id: 3, text: 'تخطي المفهوم', isCorrect: false),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
emit(VideoPlaybackReady(
|
||||
playbackData: fallbackPlayback,
|
||||
lessonItem: lesson,
|
||||
subject: subject,
|
||||
currentPositionSeconds: 0,
|
||||
isPlaying: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void updatePosition(int seconds) {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
|
||||
// Check if this second triggers a Socratic checkpoint
|
||||
for (var cp in currentState.playbackData.checkpoints) {
|
||||
if (!currentState.passedCheckpointIds.contains(cp.id) &&
|
||||
seconds >= cp.timestampSeconds &&
|
||||
seconds <= cp.timestampSeconds + 2) {
|
||||
AppLogger.log('🚨 Socratic Checkpoint Triggered! (${cp.questionText})', tag: 'SOCRATIC_ENGINE');
|
||||
emit(currentState.copyWith(
|
||||
currentPositionSeconds: seconds,
|
||||
isPlaying: false, // Freeze video playback
|
||||
activeCheckpoint: cp,
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
emit(currentState.copyWith(currentPositionSeconds: seconds));
|
||||
}
|
||||
}
|
||||
|
||||
void togglePlayPause() {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
|
||||
emit(currentState.copyWith(isPlaying: !currentState.isPlaying));
|
||||
}
|
||||
}
|
||||
|
||||
void seekTo(int seconds) {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady) {
|
||||
emit(currentState.copyWith(currentPositionSeconds: seconds));
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit Socratic Checkpoint Answer
|
||||
bool submitCheckpointAnswer(SocraticOptionModel selectedOption) {
|
||||
final currentState = state;
|
||||
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint != null) {
|
||||
final cp = currentState.activeCheckpoint!;
|
||||
|
||||
if (selectedOption.isCorrect) {
|
||||
// Correct Answer -> Reward readiness score (+0.5%) & resume video
|
||||
AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE');
|
||||
final updatedPassed = Set<int>.from(currentState.passedCheckpointIds)..add(cp.id);
|
||||
emit(currentState.copyWith(
|
||||
clearActiveCheckpoint: true,
|
||||
isPlaying: true,
|
||||
readinessBonusAdded: currentState.readinessBonusAdded + 0.5,
|
||||
passedCheckpointIds: updatedPassed,
|
||||
remediationNotice: 'إجابة نموذجية ممتازة! تم تعزيز مؤشر الجاهزية (+0.5%) 🚀',
|
||||
));
|
||||
return true;
|
||||
} else {
|
||||
// Wrong Answer -> Socratic Productive Struggle: Rewind video by N seconds
|
||||
final rewindTo = (currentState.currentPositionSeconds - cp.rewindSecondsOnFail).clamp(0, currentState.playbackData.durationSeconds);
|
||||
AppLogger.log('❌ Incorrect answer. Socratic remediation: Rewinding to ${rewindTo}s.', tag: 'SOCRATIC_ENGINE');
|
||||
emit(currentState.copyWith(
|
||||
clearActiveCheckpoint: true,
|
||||
currentPositionSeconds: rewindTo,
|
||||
isPlaying: true,
|
||||
remediationNotice: 'تعثرت في هذا المفهوم. تم إرجاع الفيديو ${cp.rewindSecondsOnFail} ثانية لإعادة الاستماع بتركيز 🔄',
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user