feat: STEM Virtual Lab, English TTS companion, dynamic textbooks and zero-demo video fallback
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user