Update Saqel Platform: 2026-09-02 09:06:45
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user