diff --git a/apps/student_app/lib/logic/cubits/video_playback_cubit.dart b/apps/student_app/lib/logic/cubits/video_playback_cubit.dart index f86e6c4..1d423e7 100644 --- a/apps/student_app/lib/logic/cubits/video_playback_cubit.dart +++ b/apps/student_app/lib/logic/cubits/video_playback_cubit.dart @@ -69,6 +69,7 @@ class VideoPlaybackError extends VideoPlaybackState { class VideoPlaybackCubit extends Cubit { final CurriculumRepository _repo; + int _lastObservedPosition = -1; VideoPlaybackCubit({CurriculumRepository? repo}) : _repo = repo ?? CurriculumRepository(), @@ -76,6 +77,7 @@ class VideoPlaybackCubit extends Cubit { Future loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async { AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT'); + _lastObservedPosition = -1; emit(VideoPlaybackLoading()); try { final playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id); @@ -97,9 +99,9 @@ class VideoPlaybackCubit extends Cubit { 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) { + final crossedCheckpoint = seconds >= cp.timestampSeconds && + (_lastObservedPosition < cp.timestampSeconds || _lastObservedPosition > seconds); + if (!currentState.passedCheckpointIds.contains(cp.id) && crossedCheckpoint) { AppLogger.log('🚨 Socratic Checkpoint Triggered! (${cp.questionText})', tag: 'SOCRATIC_ENGINE'); emit(currentState.copyWith( currentPositionSeconds: seconds, @@ -110,6 +112,7 @@ class VideoPlaybackCubit extends Cubit { } } + _lastObservedPosition = seconds; emit(currentState.copyWith(currentPositionSeconds: seconds)); } } diff --git a/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart b/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart index 182026b..389d77c 100644 --- a/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart +++ b/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart @@ -32,6 +32,17 @@ class _SocraticVideoPlayerScreenState extends State w VideoPlayerController? _videoController; bool _isVideoInitialized = false; 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 void initState() { @@ -66,6 +77,7 @@ class _SocraticVideoPlayerScreenState extends State w @override void dispose() { _playbackTicker?.cancel(); + _controlsHideTimer?.cancel(); _watermarkController.dispose(); _videoController?.dispose(); super.dispose(); @@ -131,6 +143,7 @@ class _SocraticVideoPlayerScreenState extends State w setState(() { _isVideoInitialized = true; }); + _revealVideoControls(); if (state.isPlaying && state.activeCheckpoint == null) { _videoController!.play(); } @@ -220,7 +233,12 @@ class _SocraticVideoPlayerScreenState extends State w ), ], ), - child: ClipRRect( + child: MouseRegion( + onHover: (_) => _revealVideoControls(), + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _revealVideoControls, + child: ClipRRect( borderRadius: BorderRadius.circular(24), child: Stack( children: [ @@ -263,7 +281,7 @@ class _SocraticVideoPlayerScreenState extends State w ), ), // Center Lesson Title & Visuals - Center( + if (_showVideoControls) Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -342,7 +360,7 @@ class _SocraticVideoPlayerScreenState extends State w ), ), // Top Version Badge - Positioned( + if (_showVideoControls) Positioned( top: 14, right: 14, child: Container( @@ -397,8 +415,13 @@ class _SocraticVideoPlayerScreenState extends State w }, ), - // Video Controls Overlay (Bottom) - Align( + // Video Controls Overlay (Bottom), hidden after inactivity. + IgnorePointer( + ignoring: !_showVideoControls, + child: AnimatedOpacity( + opacity: _showVideoControls ? 1 : 0, + duration: const Duration(milliseconds: 220), + child: Align( alignment: Alignment.bottomCenter, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -498,11 +521,15 @@ class _SocraticVideoPlayerScreenState extends State w ], ), ), + ), + ), ), ], + ), ), ), ), + ), const SizedBox(height: 20), // Remediation Feedback Alert (if triggered) diff --git a/apps/student_app/lib/presentation/widgets/socratic_dialog.dart b/apps/student_app/lib/presentation/widgets/socratic_dialog.dart index 41d5685..4ff9f0f 100644 --- a/apps/student_app/lib/presentation/widgets/socratic_dialog.dart +++ b/apps/student_app/lib/presentation/widgets/socratic_dialog.dart @@ -53,7 +53,7 @@ class _SocraticCheckpointModalState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Top Badge: Socratic Gatekeeping Indicator + // Top Badge: understanding checkpoint indicator Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -70,7 +70,7 @@ class _SocraticCheckpointModalState extends State { Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 14), SizedBox(width: 6), Text( - 'فحص الفهم السقراطي الذكي 🧠', + 'نقطة فهم 🧠', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700,