Update Saqel Platform: 2026-09-02 09:06:45

This commit is contained in:
Hamza-Ayed
2026-09-02 09:06:45 +03:00
parent 8c62d81f04
commit ab482d85e4
3 changed files with 40 additions and 10 deletions
@@ -69,6 +69,7 @@ class VideoPlaybackError extends VideoPlaybackState {
class VideoPlaybackCubit extends Cubit<VideoPlaybackState> { class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
final CurriculumRepository _repo; final CurriculumRepository _repo;
int _lastObservedPosition = -1;
VideoPlaybackCubit({CurriculumRepository? repo}) VideoPlaybackCubit({CurriculumRepository? repo})
: _repo = repo ?? CurriculumRepository(), : _repo = repo ?? CurriculumRepository(),
@@ -76,6 +77,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async { Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async {
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT'); AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
_lastObservedPosition = -1;
emit(VideoPlaybackLoading()); emit(VideoPlaybackLoading());
try { try {
final playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id); final playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id);
@@ -97,9 +99,9 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) { if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
// Check if this second triggers a Socratic checkpoint // Check if this second triggers a Socratic checkpoint
for (var cp in currentState.playbackData.checkpoints) { for (var cp in currentState.playbackData.checkpoints) {
if (!currentState.passedCheckpointIds.contains(cp.id) && final crossedCheckpoint = seconds >= cp.timestampSeconds &&
seconds >= cp.timestampSeconds && (_lastObservedPosition < cp.timestampSeconds || _lastObservedPosition > seconds);
seconds <= cp.timestampSeconds + 2) { if (!currentState.passedCheckpointIds.contains(cp.id) && crossedCheckpoint) {
AppLogger.log('🚨 Socratic Checkpoint Triggered! (${cp.questionText})', tag: 'SOCRATIC_ENGINE'); AppLogger.log('🚨 Socratic Checkpoint Triggered! (${cp.questionText})', tag: 'SOCRATIC_ENGINE');
emit(currentState.copyWith( emit(currentState.copyWith(
currentPositionSeconds: seconds, currentPositionSeconds: seconds,
@@ -110,6 +112,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
} }
} }
_lastObservedPosition = seconds;
emit(currentState.copyWith(currentPositionSeconds: seconds)); emit(currentState.copyWith(currentPositionSeconds: seconds));
} }
} }
@@ -32,6 +32,17 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
VideoPlayerController? _videoController; VideoPlayerController? _videoController;
bool _isVideoInitialized = false; bool _isVideoInitialized = false;
int _lastSyncedPosition = -1; int _lastSyncedPosition = -1;
Timer? _controlsHideTimer;
bool _showVideoControls = true;
void _revealVideoControls() {
if (!mounted) return;
setState(() => _showVideoControls = true);
_controlsHideTimer?.cancel();
_controlsHideTimer = Timer(const Duration(seconds: 2), () {
if (mounted) setState(() => _showVideoControls = false);
});
}
@override @override
void initState() { void initState() {
@@ -66,6 +77,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
@override @override
void dispose() { void dispose() {
_playbackTicker?.cancel(); _playbackTicker?.cancel();
_controlsHideTimer?.cancel();
_watermarkController.dispose(); _watermarkController.dispose();
_videoController?.dispose(); _videoController?.dispose();
super.dispose(); super.dispose();
@@ -131,6 +143,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
setState(() { setState(() {
_isVideoInitialized = true; _isVideoInitialized = true;
}); });
_revealVideoControls();
if (state.isPlaying && state.activeCheckpoint == null) { if (state.isPlaying && state.activeCheckpoint == null) {
_videoController!.play(); _videoController!.play();
} }
@@ -220,7 +233,12 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
), ),
], ],
), ),
child: ClipRRect( child: MouseRegion(
onHover: (_) => _revealVideoControls(),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: _revealVideoControls,
child: ClipRRect(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
child: Stack( child: Stack(
children: [ children: [
@@ -263,7 +281,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
), ),
), ),
// Center Lesson Title & Visuals // Center Lesson Title & Visuals
Center( if (_showVideoControls) Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -342,7 +360,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
), ),
), ),
// Top Version Badge // Top Version Badge
Positioned( if (_showVideoControls) Positioned(
top: 14, top: 14,
right: 14, right: 14,
child: Container( child: Container(
@@ -397,8 +415,13 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
}, },
), ),
// Video Controls Overlay (Bottom) // Video Controls Overlay (Bottom), hidden after inactivity.
Align( IgnorePointer(
ignoring: !_showVideoControls,
child: AnimatedOpacity(
opacity: _showVideoControls ? 1 : 0,
duration: const Duration(milliseconds: 220),
child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
@@ -498,11 +521,15 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
], ],
), ),
), ),
),
),
), ),
], ],
),
), ),
), ),
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
// Remediation Feedback Alert (if triggered) // Remediation Feedback Alert (if triggered)
@@ -53,7 +53,7 @@ class _SocraticCheckpointModalState extends State<SocraticCheckpointModal> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Top Badge: Socratic Gatekeeping Indicator // Top Badge: understanding checkpoint indicator
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -70,7 +70,7 @@ class _SocraticCheckpointModalState extends State<SocraticCheckpointModal> {
Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 14), Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 14),
SizedBox(width: 6), SizedBox(width: 6),
Text( Text(
'فحص الفهم السقراطي الذكي 🧠', 'نقطة فهم 🧠',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,