Enhance video playback cubit resilience and K-12 curriculum flow
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../../logic/cubits/auth_cubit.dart';
|
||||
import '../../../logic/cubits/video_playback_cubit.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
import '../../widgets/socratic_dialog.dart';
|
||||
|
||||
class SocraticVideoPlayerScreen extends StatefulWidget {
|
||||
final CurriculumLessonItemModel lesson;
|
||||
final SubjectModel? subject;
|
||||
|
||||
const SocraticVideoPlayerScreen({
|
||||
super.key,
|
||||
required this.lesson,
|
||||
this.subject,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SocraticVideoPlayerScreen> createState() => _SocraticVideoPlayerScreenState();
|
||||
}
|
||||
|
||||
class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> with SingleTickerProviderStateMixin {
|
||||
Timer? _playbackTicker;
|
||||
late AnimationController _watermarkController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<VideoPlaybackCubit>().loadLesson(widget.lesson, subject: widget.subject);
|
||||
|
||||
// Dynamic Floating Forensic Anti-Piracy Watermark Animation
|
||||
_watermarkController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 18),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
// Ticker simulating video playback time progression and Socratic checkpoint gatekeeping
|
||||
_playbackTicker = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
final cubit = context.read<VideoPlaybackCubit>();
|
||||
final state = cubit.state;
|
||||
if (state is VideoPlaybackReady && state.isPlaying && state.activeCheckpoint == null) {
|
||||
if (state.currentPositionSeconds < state.playbackData.durationSeconds) {
|
||||
cubit.updatePosition(state.currentPositionSeconds + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_playbackTicker?.cancel();
|
||||
_watermarkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatTime(int seconds) {
|
||||
final m = seconds ~/ 60;
|
||||
final s = seconds % 60;
|
||||
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = context.read<AuthCubit>().state;
|
||||
final studentName = authState is Authenticated ? authState.user.name : 'طالب صَقِل';
|
||||
final studentPhone = authState is Authenticated ? authState.user.phone : '0798583052';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(CupertinoIcons.back, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(
|
||||
widget.lesson.title,
|
||||
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.shield_lefthalf_fill, color: AppColors.saqelCyan, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'حماية جنائية DRM',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocConsumer<VideoPlaybackCubit, VideoPlaybackState>(
|
||||
listener: (context, state) {
|
||||
if (state is VideoPlaybackReady) {
|
||||
// Trigger Socratic Checkpoint Modal automatically when active
|
||||
if (state.activeCheckpoint != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (modalContext) => SocraticCheckpointModal(
|
||||
checkpoint: state.activeCheckpoint!,
|
||||
onOptionSelected: (opt) {
|
||||
final isCorrect = context.read<VideoPlaybackCubit>().submitCheckpointAnswer(opt);
|
||||
if (isCorrect) {
|
||||
SaqelToast.showSuccess(context, 'إجابة نموذجية! تم تعزيز الجاهزية (+0.5%)', title: 'فحص الفهم السقراطي ✨');
|
||||
} else {
|
||||
SaqelToast.showError(context, 'تم إرجاع الفيديو لإعادة الاستماع للنقطة الجوهرية 🔄', title: 'سد الثغرة العلاجية');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is VideoPlaybackLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.saqelCyan),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is VideoPlaybackError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.exclamationmark_triangle_fill, color: AppColors.crimsonRed, size: 48),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message, style: const TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: 16),
|
||||
LuxuryButton(
|
||||
text: 'إعادة المحاولة',
|
||||
onPressed: () => context.read<VideoPlaybackCubit>().loadLesson(widget.lesson),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is VideoPlaybackReady) {
|
||||
final duration = state.playbackData.durationSeconds > 0 ? state.playbackData.durationSeconds : 960;
|
||||
final current = state.currentPositionSeconds;
|
||||
final progress = (current / duration).clamp(0.0, 1.0);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1000),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Video Viewport Canvas with Anti-Piracy Floating Watermark
|
||||
Container(
|
||||
height: 380,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x66000000),
|
||||
blurRadius: 30,
|
||||
offset: Offset(0, 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video Background Scene Simulation
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: Alignment(0.0, -0.2),
|
||||
radius: 1.2,
|
||||
colors: [
|
||||
Color(0xFF0F1E36),
|
||||
Color(0xFF050B14),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(25),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(100), width: 2),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
state.isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill,
|
||||
color: AppColors.saqelCyan,
|
||||
size: 36,
|
||||
),
|
||||
onPressed: () => context.read<VideoPlaybackCubit>().togglePlayPause(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.lesson.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'بث مشفر فائق الجودة HLS 1080p — منصة صَقِل',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondaryDark,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Dynamic Forensic Anti-Piracy Watermark (Moves continuously)
|
||||
AnimatedBuilder(
|
||||
animation: _watermarkController,
|
||||
builder: (context, child) {
|
||||
return Align(
|
||||
alignment: Alignment(
|
||||
-0.8 + (1.6 * _watermarkController.value),
|
||||
-0.7 + (1.4 * _watermarkController.value),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(100),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'$studentName • $studentPhone',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white.withAlpha(90),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Video Controls Overlay (Bottom)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Color(0xE6000000)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Socratic Timeline Progress Bar with Checkpoint Markers
|
||||
Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 5,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
|
||||
activeTrackColor: AppColors.saqelCyan,
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: AppColors.saqelCyan,
|
||||
),
|
||||
child: Slider(
|
||||
value: progress,
|
||||
onChanged: (val) {
|
||||
final targetSec = (val * duration).toInt();
|
||||
context.read<VideoPlaybackCubit>().seekTo(targetSec);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Checkpoint markers on the progress track
|
||||
...state.playbackData.checkpoints.map((cp) {
|
||||
final posFraction = (cp.timestampSeconds / duration).clamp(0.0, 1.0);
|
||||
final isPassed = state.passedCheckpointIds.contains(cp.id);
|
||||
return Positioned(
|
||||
left: (MediaQuery.of(context).size.width * 0.85 * posFraction).clamp(20.0, 700.0),
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber).withAlpha(120),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
state.isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => context.read<VideoPlaybackCubit>().togglePlayPause(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${_formatTime(current)} / ${_formatTime(duration)}',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${state.passedCheckpointIds.length}/${state.playbackData.checkpoints.length} فحص منجز',
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Remediation Feedback Alert (if triggered)
|
||||
if (state.remediationNotice != null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.remediationNotice!,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Lesson Meta & Learning Outcomes
|
||||
LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'نتاجات التعلم والمفاهيم الوزارية 🎯',
|
||||
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.appleBlue.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
widget.subject?.title ?? 'المنهاج الرسمي',
|
||||
style: const TextStyle(color: AppColors.appleBlue, fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
if (widget.lesson.outcomes.isNotEmpty)
|
||||
...widget.lesson.outcomes.map((out) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.check_mark_circled_solid, color: AppColors.saqelCyan, size: 16),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
out,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 13, height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
else
|
||||
const Text(
|
||||
'شرح المفاهيم والقواعد الوزارية المقررة مع التطبيقات الحياتية ونماذج الامتحانات.',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user