feat: STEM Virtual Lab, English TTS companion, dynamic textbooks and zero-demo video fallback

This commit is contained in:
Hamza-Ayed
2026-09-03 17:37:15 +03:00
parent 445a1aa0e3
commit 58c3c48f69
19 changed files with 2162 additions and 258 deletions
@@ -0,0 +1,223 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import '../../core/theme/app_colors.dart';
import '../../core/utils/saqel_toast.dart';
/// ==============================================================================
/// SAQEL ENTERPRISE (EDTECH 2.0) - ENGLISH TEXT-TO-SPEECH (TTS) AUDIO COMPANION
/// ==============================================================================
///
/// ويدجت الناطق الصوتي الذكي لنصوص وقراءات ومفردات اللغة الإنجليزية:
/// 1. يعمل محلياً عبر محركات Apple AVFoundation و Google TTS فائقة الدقة.
/// 2. تحكم في سرعة النطق (0.5x للمبتدئين، 0.8x معيارية، 1.0x طبيعية).
/// 3. دعم التبديل بين اللهجة الأمريكية (en-US) والبريطانية (en-GB).
/// 4. شريط تحكم عائم فاخر بأسلوب كوبرتينو الداكن.
class EnglishTtsPlayerWidget extends StatefulWidget {
final String textToRead;
final String? title;
const EnglishTtsPlayerWidget({
super.key,
required this.textToRead,
this.title,
});
@override
State<EnglishTtsPlayerWidget> createState() => _EnglishTtsPlayerWidgetState();
}
class _EnglishTtsPlayerWidgetState extends State<EnglishTtsPlayerWidget> {
late FlutterTts _flutterTts;
bool _isPlaying = false;
double _speechRate = 0.45; // Moderate pace ideal for ESL learners
String _selectedLanguage = 'en-US';
@override
void initState() {
super.initState();
_initTts();
}
Future<void> _initTts() async {
_flutterTts = FlutterTts();
await _flutterTts.setLanguage(_selectedLanguage);
await _flutterTts.setSpeechRate(_speechRate);
await _flutterTts.setVolume(1.0);
await _flutterTts.setPitch(1.0);
_flutterTts.setStartHandler(() {
if (mounted) setState(() => _isPlaying = true);
});
_flutterTts.setCompletionHandler(() {
if (mounted) setState(() => _isPlaying = false);
});
_flutterTts.setCancelHandler(() {
if (mounted) setState(() => _isPlaying = false);
});
_flutterTts.setErrorHandler((msg) {
if (mounted) {
setState(() => _isPlaying = false);
SaqelToast.showError(context, 'تعذر تشغيل الصوت: $msg', title: 'الناطق الصوتي');
}
});
}
@override
void dispose() {
_flutterTts.stop();
super.dispose();
}
Future<void> _togglePlayPause() async {
if (_isPlaying) {
await _flutterTts.stop();
if (mounted) setState(() => _isPlaying = false);
} else {
if (widget.textToRead.trim().isEmpty) {
SaqelToast.showInfo(context, 'لا يوجد نص للقراءة حالياً', title: 'تنبيه');
return;
}
await _flutterTts.setLanguage(_selectedLanguage);
await _flutterTts.setSpeechRate(_speechRate);
await _flutterTts.speak(widget.textToRead);
}
}
Future<void> _cycleSpeed() async {
setState(() {
if (_speechRate <= 0.35) {
_speechRate = 0.48; // Normal
} else if (_speechRate <= 0.50) {
_speechRate = 0.65; // Fast
} else {
_speechRate = 0.30; // Slow
}
});
await _flutterTts.setSpeechRate(_speechRate);
final label = _speechRate < 0.4 ? 'بطيء (0.5x)' : (_speechRate < 0.6 ? 'معياري (0.8x)' : 'طبيعي (1.0x)');
if (mounted) SaqelToast.showInfo(context, 'سرعة القراءة: $label', title: 'سرعة النطق');
}
Future<void> _toggleAccent() async {
setState(() {
_selectedLanguage = _selectedLanguage == 'en-US' ? 'en-GB' : 'en-US';
});
await _flutterTts.setLanguage(_selectedLanguage);
final accentName = _selectedLanguage == 'en-US' ? 'أمريكية (US 🇺🇸)' : 'بريطانية (UK 🇬🇧)';
if (mounted) SaqelToast.showInfo(context, 'تم اختيار اللهجة: $accentName', title: 'لهجة القراءة');
}
@override
Widget build(BuildContext context) {
final speedLabel = _speechRate < 0.4 ? '0.5x' : (_speechRate < 0.6 ? '0.8x' : '1.0x');
final accentLabel = _selectedLanguage == 'en-US' ? 'US 🇺🇸' : 'UK 🇬🇧';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
boxShadow: [
BoxShadow(
color: AppColors.saqelCyan.withAlpha(20),
blurRadius: 16,
spreadRadius: 1,
),
],
),
child: Row(
children: [
// Play / Stop Button
GestureDetector(
onTap: _togglePlayPause,
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: _isPlaying ? AppColors.crimsonRed : AppColors.saqelCyan,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: (_isPlaying ? AppColors.crimsonRed : AppColors.saqelCyan).withAlpha(80),
blurRadius: 10,
),
],
),
child: Icon(
_isPlaying ? CupertinoIcons.stop_fill : CupertinoIcons.volume_up,
color: Colors.black,
size: 20,
),
),
),
const SizedBox(width: 12),
// Title & Status
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.title ?? 'الناطق الصوتي الذكي (English Audio Reader)',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12.5),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
_isPlaying ? 'جاري الاستماع بنقاء صوتي عالي... 🎧' : 'اضغط للاستماع للنطق السليم للدرس',
style: TextStyle(
color: _isPlaying ? AppColors.saqelCyan : AppColors.textSecondaryDark,
fontSize: 11,
fontWeight: _isPlaying ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
// Speed Control Chip
GestureDetector(
onTap: _cycleSpeed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withAlpha(15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white24),
),
child: Text(
speedLabel,
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700),
),
),
),
const SizedBox(width: 6),
// Accent Control Chip
GestureDetector(
onTap: _toggleAccent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(20),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
),
child: Text(
accentLabel,
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
),
),
),
],
),
);
}
}