Enhance video playback cubit resilience and K-12 curriculum flow

This commit is contained in:
Hamza-Ayed
2026-08-31 17:46:03 +03:00
parent 78b703f625
commit d8a50bb105
34 changed files with 2934 additions and 75 deletions
@@ -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();
},
),
);
}
}