feat(curriculum): restore video player, organize math & english labs hierarchically, add scoped audio and interactive grammar mind maps
This commit is contained in:
+176
-56
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user