feat(curriculum): restore video player, organize math & english labs hierarchically, add scoped audio and interactive grammar mind maps

This commit is contained in:
Hamza-Ayed
2026-09-05 00:33:45 +03:00
parent b0c2bfc03b
commit 368f68cc97
9 changed files with 2939 additions and 163 deletions
@@ -57,6 +57,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
int _lastSyncedPosition = -1;
Timer? _controlsHideTimer;
bool _showVideoControls = true;
bool _showChalkboardMode = false;
final FlutterTts _tts = FlutterTts();
bool _isSpeakingChalkboard = false;
@@ -64,11 +65,49 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
if (!mounted) return;
setState(() => _showVideoControls = true);
_controlsHideTimer?.cancel();
_controlsHideTimer = Timer(const Duration(seconds: 2), () {
_controlsHideTimer = Timer(const Duration(seconds: 3), () {
if (mounted) setState(() => _showVideoControls = false);
});
}
void _initPlayer(String videoUrl, int resumePos, bool shouldPlay) {
final effectiveUrl = videoUrl.isNotEmpty
? videoUrl
: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
_videoController?.dispose();
_videoController = VideoPlayerController.networkUrl(Uri.parse(effectiveUrl))
..initialize().then((_) {
if (mounted) {
setState(() {
_isVideoInitialized = true;
_videoInitError = null;
});
_revealVideoControls();
if (resumePos > 3) {
_videoController!.seekTo(Duration(seconds: resumePos));
if (context.mounted) {
SaqelToast.showInfo(
context,
'تم استئناف المشاهدة من الدقيقة ${_formatTime(resumePos)} ⏱️',
title: 'استئناف المشاهدة',
);
}
}
if (shouldPlay) {
_videoController!.play();
}
}
}).catchError((err) {
if (mounted) {
setState(() {
_isVideoInitialized = false;
_videoInitError = 'تعذر تحميل بث الفيديو المباشر؛ انقر لإعادة المحاولة';
});
}
});
}
Future<void> _speakConcept(String text) async {
if (text.isEmpty) return;
setState(() => _isSpeakingChalkboard = true);
@@ -177,37 +216,12 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
listener: (context, state) {
if (state is VideoPlaybackReady) {
// Initialize Video Player if not yet initialized
if (_videoController == null) {
_videoController = VideoPlayerController.networkUrl(Uri.parse(state.playbackData.videoUrl))
..initialize().then((_) {
if (mounted) {
setState(() {
_isVideoInitialized = true;
_videoInitError = null;
});
_revealVideoControls();
if (state.currentPositionSeconds > 3) {
_videoController!.seekTo(Duration(seconds: state.currentPositionSeconds));
if (context.mounted) {
SaqelToast.showInfo(
context,
'تم استئناف المشاهدة من الدقيقة ${_formatTime(state.currentPositionSeconds)} ⏱️',
title: 'استئناف المشاهدة',
);
}
}
if (state.isPlaying && state.activeCheckpoint == null) {
_videoController!.play();
}
}
}).catchError((err) {
if (mounted) {
setState(() {
_isVideoInitialized = false;
_videoInitError = 'الفيديو قيد المزامنة والتجهيز على خادم البث المباشر';
});
}
});
if (_videoController == null && _videoInitError == null) {
_initPlayer(
state.playbackData.videoUrl,
state.currentPositionSeconds,
state.isPlaying && state.activeCheckpoint == null,
);
} else if (_isVideoInitialized) {
// Sync play/pause state
if (state.isPlaying && state.activeCheckpoint == null) {
@@ -215,7 +229,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
} else {
_videoController!.pause();
}
if (state.currentPositionSeconds != _videoController!.value.position.inSeconds) {
if ((state.currentPositionSeconds - _videoController!.value.position.inSeconds).abs() > 2) {
_videoController!.seekTo(Duration(seconds: state.currentPositionSeconds));
}
}
@@ -340,10 +354,82 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
),
),
// Center Lesson Title & Visuals / Interactive Socratic Chalkboard
if (_videoInitError != null || !_isVideoInitialized || state.playbackData.videoUrl.isEmpty)
if (_showChalkboardMode)
Positioned.fill(
child: _buildInteractiveChalkboard(state),
)
else if (_videoInitError != null)
Positioned.fill(
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withAlpha(210),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.saqelCyan.withAlpha(90)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(CupertinoIcons.play_circle_fill, color: AppColors.saqelCyan, size: 50),
const SizedBox(height: 12),
Text(
widget.lesson.title,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
_videoInitError!,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
const SizedBox(height: 14),
ElevatedButton.icon(
icon: const Icon(CupertinoIcons.arrow_clockwise, size: 16),
label: const Text('بدء تشغيل بث الفيديو المباشر 🎥'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
setState(() {
_videoInitError = null;
_isVideoInitialized = false;
});
_initPlayer(
state.playbackData.videoUrl.isNotEmpty
? state.playbackData.videoUrl
: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
state.currentPositionSeconds,
true,
);
},
),
],
),
),
),
)
else if (!_isVideoInitialized)
Positioned.fill(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CupertinoActivityIndicator(color: AppColors.saqelCyan, radius: 18),
const SizedBox(height: 14),
Text(
'جاري تهيئة البث المباشر للشرح (${widget.lesson.title}) ⚡',
style: const TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w600),
),
],
),
),
)
else if (_showVideoControls) Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -422,30 +508,64 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
],
),
),
// Top Version Badge
if (_showVideoControls) Positioned(
top: 14,
right: 14,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.black.withAlpha(140),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 12),
SizedBox(width: 6),
Text(
'نقاط الفهم',
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
),
],
// Top Action Badges & Mode Switcher
if (_showVideoControls) ...[
Positioned(
top: 14,
right: 14,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.black.withAlpha(140),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 12),
SizedBox(width: 6),
Text(
'نقاط الفهم',
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
),
),
),
Positioned(
top: 14,
left: 14,
child: GestureDetector(
onTap: () {
setState(() => _showChalkboardMode = !_showChalkboardMode);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.black.withAlpha(160),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.saqelCyan.withAlpha(90)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_showChalkboardMode ? CupertinoIcons.film : CupertinoIcons.square_pencil,
color: AppColors.saqelCyan,
size: 13,
),
const SizedBox(width: 6),
Text(
_showChalkboardMode ? 'عرض الفيديو 🎥' : 'لوحة الشرح 📝',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
),
),
),
],
],
),