Files
saqel/apps/student_app/lib/logic/cubits/video_playback_cubit.dart
T

189 lines
7.3 KiB
Dart

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.markdownFilePath ?? lesson.id, subjectId: subject?.id);
emit(VideoPlaybackReady(
playbackData: playback,
lessonItem: lesson,
subject: subject,
currentPositionSeconds: 0,
isPlaying: true,
));
} catch (e) {
AppLogger.log('Playback API failed for ${lesson.title}: $e', tag: 'VIDEO_CUBIT');
emit(VideoPlaybackError('لا يوجد فيديو منشور لهذا الدرس حاليًا. يرجى المحاولة لاحقًا.'));
}
}
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));
}
}
Future<void> saveProgress({required int positionSeconds, required int watchedSeconds}) async {
final currentState = state;
if (currentState is! VideoPlaybackReady || currentState.playbackData.lessonId <= 0) return;
try {
await _repo.saveProgress(
lessonId: currentState.playbackData.lessonId,
positionSeconds: positionSeconds,
watchedSeconds: watchedSeconds,
);
} catch (e) {
AppLogger.log('Progress sync deferred: $e', tag: 'VIDEO_CUBIT');
}
}
/// 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%) 🚀',
));
if (cp.id > 0) {
_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
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} ثانية لإعادة الاستماع بتركيز 🔄',
));
if (cp.id > 0 && cp.questionId > 0) {
_repo.submitCheckpoint(examId: cp.id, questionId: cp.questionId, optionId: selectedOption.id).catchError((e) {
AppLogger.log('Checkpoint sync deferred: $e', tag: 'VIDEO_CUBIT');
});
}
return false;
}
}
return false;
}
}