Update Saqel Platform: 2026-09-05 22:17:15

This commit is contained in:
Hamza-Ayed
2026-09-05 22:17:15 +03:00
parent 68265a5bc6
commit 0e5be7adbb
6 changed files with 3322 additions and 885 deletions
@@ -49,30 +49,49 @@ class EnglishReadingService {
if (_isInitialized) return; if (_isInitialized) return;
try { try {
await _tts.setLanguage('en-US'); await _tts.setLanguage('en-GB');
await _tts.setSpeechRate(0.44); // Standard natural reading speed for learners await _tts.setSpeechRate(0.40); // British English natural learner cadence
await _tts.setVolume(1.0); await _tts.setVolume(1.0);
await _tts.setPitch(1.0); await _tts.setPitch(1.0);
// Attempt to find and select enhanced/natural high-fidelity voice // Select highest fidelity British English (en-GB / RP) voice
try { try {
final voices = await _tts.getVoices; final voices = await _tts.getVoices;
if (voices is List) { if (voices is List) {
Map? preferredVoice;
Map? fallbackGbVoice;
for (var v in voices) { for (var v in voices) {
if (v is Map) { if (v is Map) {
final name = (v['name'] ?? '').toString().toLowerCase(); final name = (v['name'] ?? '').toString().toLowerCase();
final locale = (v['locale'] ?? '').toString().toLowerCase(); final locale = (v['locale'] ?? '').toString().toLowerCase();
if ((locale.contains('en-us') || locale.contains('en_us')) &&
(name.contains('natural') || name.contains('enhanced') || name.contains('samantha') || name.contains('neural'))) { if (locale.contains('en-gb') || locale.contains('en_gb')) {
await _tts.setVoice({'name': v['name'], 'locale': v['locale']}); fallbackGbVoice ??= v;
AppLogger.log('Selected high-quality natural voice: ${v['name']}', tag: 'ENGLISH_AUDIO'); if (name.contains('natural') ||
break; name.contains('enhanced') ||
name.contains('neural') ||
name.contains('premium') ||
name.contains('oliver') ||
name.contains('george') ||
name.contains('serena') ||
name.contains('daniel') ||
name.contains('kate')) {
preferredVoice = v;
break;
}
} }
} }
} }
final selectedVoice = preferredVoice ?? fallbackGbVoice;
if (selectedVoice != null) {
await _tts.setVoice({'name': selectedVoice['name'], 'locale': selectedVoice['locale']});
AppLogger.log('Selected British English (en-GB) voice: ${selectedVoice['name']}', tag: 'ENGLISH_AUDIO');
}
} }
} catch (e) { } catch (e) {
AppLogger.log('Voice auto-tuning warning: $e', tag: 'ENGLISH_AUDIO'); AppLogger.log('British voice auto-tuning warning: $e', tag: 'ENGLISH_AUDIO');
} }
_tts.setStartHandler(() { _tts.setStartHandler(() {
@@ -104,7 +123,7 @@ class EnglishReadingService {
} }
} }
/// قراءة فقرة محددة مع إيقاف أي قراءة جارية /// قراءة فقرة محددة بالنموذج البريطاني النقي مع إيقاف أي قراءة جارية
Future<void> readParagraph({ Future<void> readParagraph({
required String paragraphId, required String paragraphId,
required String text, required String text,
@@ -134,11 +153,12 @@ class EnglishReadingService {
return; return;
} }
await _tts.setSpeechRate(0.44); await _tts.setLanguage('en-GB');
await _tts.setSpeechRate(0.40);
await _tts.speak(cleanText); await _tts.speak(cleanText);
} }
/// نطق كلمة واحدة مفردة بوضوح صوتي نقي /// نطق كلمة واحدة مفردة بمخارج حروف دقيقة جداً (British RP Articulation)
Future<void> speakWord(String word) async { Future<void> speakWord(String word) async {
await initialize(); await initialize();
if (word.trim().contains(' ')) { if (word.trim().contains(' ')) {
@@ -149,11 +169,12 @@ class EnglishReadingService {
if (cleanWord.isEmpty) return; if (cleanWord.isEmpty) return;
await _tts.stop(); await _tts.stop();
await _tts.setSpeechRate(0.38); // Slightly slower for distinct phonetic articulation await _tts.setLanguage('en-GB');
await _tts.setSpeechRate(0.35); // Slowed down slightly for clear phonetic articulation
await _tts.speak(cleanWord); await _tts.speak(cleanWord);
} }
/// نطق جملة أو عبارة تعليمية بوضوح وصوت طبيعي /// نطق جملة أو عبارة تعليمية باللكنة البريطانية المعتمدة
Future<void> speakSentence(String text) async { Future<void> speakSentence(String text) async {
await initialize(); await initialize();
final cleanText = text final cleanText = text
@@ -163,7 +184,8 @@ class EnglishReadingService {
if (cleanText.isEmpty) return; if (cleanText.isEmpty) return;
await _tts.stop(); await _tts.stop();
await _tts.setSpeechRate(0.44); await _tts.setLanguage('en-GB');
await _tts.setSpeechRate(0.40);
await _tts.speak(cleanText); await _tts.speak(cleanText);
} }
@@ -532,18 +532,18 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(30), color: const Color(0xFF1E3A8A).withAlpha(140),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.appleBlue.withAlpha(90)), border: Border.all(color: const Color(0xFF60A5FA).withAlpha(160)),
), ),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(CupertinoIcons.speaker_2_fill, color: AppColors.appleBlue, size: 12), Text('🇬🇧', style: TextStyle(fontSize: 12)),
SizedBox(width: 4), SizedBox(width: 5),
Text( Text(
'صوتيات ذكية (IPA & TTS)', 'صوت بريطاني فصيح (en-GB RP)',
style: TextStyle(color: AppColors.appleBlue, fontSize: 11, fontWeight: FontWeight.w700), style: TextStyle(color: Color(0xFF93C5FD), fontSize: 11, fontWeight: FontWeight.w700),
), ),
], ],
), ),
@@ -929,27 +929,29 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
// ============================================================================ // ============================================================================
// TAB 2: PHONETICS & PRONUNCIATION STUDIO (IPA & TTS) // TAB 2: PHONETICS & PRONUNCIATION STUDIO (IPA & TTS)
// ============================================================================ // ============================================================================
// ============================================================================
// TAB 2: PHONETICS & PRONUNCIATION STUDIO (BRITISH RP & TTS)
// ============================================================================
Widget _buildPhoneticsStudioView() { Widget _buildPhoneticsStudioView() {
final activeUnit = _vocabUnits[_selectedVocabUnitIndex]; final activeUnit = _vocabUnits[_selectedVocabUnitIndex];
final words = activeUnit['words'] as List; final words = activeUnit['words'] as List;
final activeWord = words[_selectedWordIndex] as Map<String, dynamic>; final activeWord = words[_selectedWordIndex] as Map<String, dynamic>;
return Row( return LayoutBuilder(
children: [ builder: (context, constraints) {
// Left: Units Selector & Words List final isCompact = constraints.maxWidth < 760;
Container(
width: 280, if (isCompact) {
decoration: const BoxDecoration( return ListView(
color: AppColors.darkSurface, padding: const EdgeInsets.all(16),
border: Border(left: BorderSide(color: AppColors.darkCardBorder)),
),
child: Column(
children: [ children: [
// Unit Dropdown // Unit selector dropdown
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
decoration: const BoxDecoration( decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)), color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.darkCardBorder),
), ),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<int>( child: DropdownButton<int>(
@@ -961,7 +963,7 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
value: i, value: i,
child: Text( child: Text(
_vocabUnits[i]['unit'] as String, _vocabUnits[i]['unit'] as String,
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold),
), ),
); );
}), }),
@@ -976,171 +978,280 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
), ),
), ),
), ),
const SizedBox(height: 12),
// Words List // Words chips list
Expanded( SingleChildScrollView(
child: ListView.builder( scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(8), child: Row(
itemCount: words.length, children: List.generate(words.length, (index) {
itemBuilder: (context, index) {
final item = words[index]; final item = words[index];
final isSelected = _selectedWordIndex == index; final isSelected = _selectedWordIndex == index;
return Padding(
return Container( padding: const EdgeInsets.only(left: 8),
margin: const EdgeInsets.only(bottom: 6), child: ChoiceChip(
decoration: BoxDecoration( label: Text(item['word'] as String),
color: isSelected ? AppColors.saqelCyan.withAlpha(25) : Colors.transparent, selected: isSelected,
borderRadius: BorderRadius.circular(10), selectedColor: AppColors.saqelCyan,
border: Border.all( labelStyle: TextStyle(
color: isSelected ? AppColors.saqelCyan : Colors.transparent, color: isSelected ? Colors.black : Colors.white,
fontWeight: FontWeight.w700,
fontSize: 12,
), ),
), backgroundColor: AppColors.darkSurface,
child: ListTile( onSelected: (_) => setState(() => _selectedWordIndex = index),
dense: true,
title: Text(
item['word'] as String,
style: TextStyle(
color: isSelected ? AppColors.saqelCyan : Colors.white,
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
),
),
subtitle: Text(
item['ar'] as String,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
trailing: IconButton(
icon: const Icon(CupertinoIcons.volume_up, size: 16, color: AppColors.saqelCyan),
onPressed: () => _speakWord(item['word'] as String),
),
onTap: () => setState(() => _selectedWordIndex = index),
), ),
); );
}, }),
), ),
), ),
const SizedBox(height: 16),
// Phonetics Card
_buildPhoneticsWorkbenchCard(activeWord),
],
);
}
return Row(
children: [
// Left: Units Selector & Words List
Container(
width: 280,
decoration: const BoxDecoration(
color: AppColors.darkSurface,
border: Border(left: BorderSide(color: AppColors.darkCardBorder)),
),
child: Column(
children: [
// Unit Dropdown
Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _selectedVocabUnitIndex,
isExpanded: true,
dropdownColor: AppColors.darkSurface,
items: List.generate(_vocabUnits.length, (i) {
return DropdownMenuItem(
value: i,
child: Text(
_vocabUnits[i]['unit'] as String,
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}),
onChanged: (val) {
if (val != null) {
setState(() {
_selectedVocabUnitIndex = val;
_selectedWordIndex = 0;
});
}
},
),
),
),
// Words List
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: words.length,
itemBuilder: (context, index) {
final item = words[index];
final isSelected = _selectedWordIndex == index;
return Container(
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
color: isSelected ? AppColors.saqelCyan.withAlpha(25) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
),
),
child: ListTile(
dense: true,
title: Text(
item['word'] as String,
style: TextStyle(
color: isSelected ? AppColors.saqelCyan : Colors.white,
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
),
),
subtitle: Text(
item['ar'] as String,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
trailing: IconButton(
icon: const Icon(CupertinoIcons.volume_up, size: 16, color: AppColors.saqelCyan),
onPressed: () => _speakWord(item['word'] as String),
),
onTap: () => setState(() => _selectedWordIndex = index),
),
);
},
),
),
],
),
),
// Right: Phonetics Audio Workbench
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: _buildPhoneticsWorkbenchCard(activeWord),
),
),
],
);
},
);
}
Widget _buildPhoneticsWorkbenchCard(Map<String, dynamic> activeWord) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Big Pronunciation Header Card
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0B192C), Color(0xFF1E293B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.saqelCyan.withAlpha(90)),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 20, offset: Offset(0, 10)),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(30),
borderRadius: BorderRadius.circular(16),
),
child: Text(
activeWord['pos'] as String,
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF1E3A8A).withAlpha(160),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF60A5FA).withAlpha(120)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('🇬🇧', style: TextStyle(fontSize: 12)),
SizedBox(width: 4),
Text(
'British RP',
style: TextStyle(color: Color(0xFF93C5FD), fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
),
],
),
const SizedBox(height: 14),
Text(
activeWord['word'] as String,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
const SizedBox(height: 6),
Text(
activeWord['ipa'] as String,
style: const TextStyle(
color: AppColors.saqelCyan,
fontSize: 19,
fontFamily: 'Courier',
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 10),
Text(
activeWord['ar'] as String,
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 18),
// Audio Play Button
ElevatedButton.icon(
icon: Icon(
_isPlayingAudio ? CupertinoIcons.waveform : CupertinoIcons.volume_up,
size: 20,
),
label: Text(
_isPlayingAudio ? 'جاري النطق البريطاني...' : 'استمع للنطق البريطاني الفصيح (British Audio Pronunciation)',
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
onPressed: () => _speakWord(activeWord['word'] as String),
),
], ],
), ),
), ),
// Right: Phonetics Audio Workbench const SizedBox(height: 20),
Expanded( // Detailed Definition & Collocations
child: SingleChildScrollView( LuxuryCard(
padding: const EdgeInsets.all(28), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ const Text('التعريف المعتمد (Official Definition):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12)),
// Big Pronunciation Header Card const SizedBox(height: 6),
Container( Text(activeWord['def'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5)),
padding: const EdgeInsets.all(28), const Divider(color: AppColors.darkCardBorder, height: 24),
decoration: BoxDecoration(
gradient: const LinearGradient( const Text('جملة المنهاج الوزاري (Textbook Context):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12)),
colors: [Color(0xFF0D1B2A), Color(0xFF1B263B)], const SizedBox(height: 6),
begin: Alignment.topLeft, Row(
end: Alignment.bottomRight, children: [
), Expanded(
borderRadius: BorderRadius.circular(24), child: Text(activeWord['sentence'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5, fontStyle: FontStyle.italic)),
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 20, offset: Offset(0, 10)),
],
), ),
child: Column( IconButton(
children: [ icon: const Icon(CupertinoIcons.speaker_1, color: AppColors.guardianAmber, size: 18),
Container( onPressed: () => _speakWord(activeWord['sentence'] as String),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), tooltip: 'استمع للجملة',
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(30),
borderRadius: BorderRadius.circular(16),
),
child: Text(
activeWord['pos'] as String,
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 14),
Text(
activeWord['word'] as String,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
const SizedBox(height: 6),
Text(
activeWord['ipa'] as String,
style: const TextStyle(
color: AppColors.saqelCyan,
fontSize: 18,
fontFamily: 'Courier',
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 10),
Text(
activeWord['ar'] as String,
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// Audio Play Button
ElevatedButton.icon(
icon: Icon(
_isPlayingAudio ? CupertinoIcons.waveform : CupertinoIcons.volume_up,
size: 20,
),
label: Text(
_isPlayingAudio ? 'جاري النطق...' : 'استمع للنطق الصوتي النقي (Audio Pronunciation)',
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
onPressed: () => _speakWord(activeWord['word'] as String),
),
],
), ),
), ],
),
const Divider(color: AppColors.darkCardBorder, height: 24),
const SizedBox(height: 20), const Text('المتلازمات اللفظية (Collocations):', style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.bold, fontSize: 12)),
// Detailed Definition & Collocations const SizedBox(height: 6),
LuxuryCard( Text(activeWord['collocations'] as String, style: const TextStyle(color: Colors.white70, fontSize: 12)),
child: Column( ],
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('التعريف المعتمد (Official Definition):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Text(activeWord['def'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5)),
const Divider(color: AppColors.darkCardBorder, height: 24),
const Text('جملة المنهاج الوزاري (Textbook Context):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Text(activeWord['sentence'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5, fontStyle: FontStyle.italic)),
),
IconButton(
icon: const Icon(CupertinoIcons.speaker_1, color: AppColors.guardianAmber, size: 18),
onPressed: () => _speakWord(activeWord['sentence'] as String),
),
],
),
const Divider(color: AppColors.darkCardBorder, height: 24),
const Text('المتلازمات اللفظية (Collocations):', style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Text(activeWord['collocations'] as String, style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
),
],
),
), ),
), ),
], ],
@@ -431,292 +431,315 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
final activeEx = _textbookExercises[_selectedExerciseIndex]; final activeEx = _textbookExercises[_selectedExerciseIndex];
final points = activeEx['points'] as List<Offset>; final points = activeEx['points'] as List<Offset>;
return Row( return LayoutBuilder(
children: [ builder: (context, constraints) {
// Left: Cartesian Graph Canvas final isMobile = constraints.maxWidth < 720;
Expanded(
flex: 6, if (isMobile) {
child: Container( return SingleChildScrollView(
margin: const EdgeInsets.all(16), child: Column(
decoration: BoxDecoration( children: [
color: const Color(0xFF070C16), SizedBox(
borderRadius: BorderRadius.circular(20), height: 330,
border: Border.all(color: AppColors.darkCardBorder), child: _buildCanvasContainer(points),
boxShadow: const [ ),
BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 8)), _buildControlsPanel(),
], ],
), ),
child: ClipRRect( );
borderRadius: BorderRadius.circular(20), }
child: Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: _CartesianCanvasPainter(
curve1Type: _curve1Type,
radiusSquared: _radiusSquared,
a1: _a1,
c1: _c1,
curve2Type: _curve2Type,
m: _m,
b: _b,
c2: _c2,
scale: _scale,
intersectionPoints: points,
),
),
),
// Controls Overlay (Zoom) return Row(
Positioned( children: [
top: 14, Expanded(
left: 14, flex: 6,
child: Container( child: _buildCanvasContainer(points),
padding: const EdgeInsets.all(4), ),
decoration: BoxDecoration( Expanded(
color: Colors.black.withAlpha(180), flex: 4,
borderRadius: BorderRadius.circular(12), child: _buildControlsPanel(),
border: Border.all(color: Colors.white24), ),
), ],
child: Row( );
mainAxisSize: MainAxisSize.min, },
children: [ );
IconButton( }
icon: const Icon(CupertinoIcons.zoom_in, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale + 4).clamp(12.0, 45.0)),
tooltip: 'تكبير',
),
IconButton(
icon: const Icon(CupertinoIcons.zoom_out, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale - 4).clamp(12.0, 45.0)),
tooltip: 'تصغير',
),
IconButton(
icon: const Icon(CupertinoIcons.refresh, color: AppColors.saqelCyan, size: 18),
onPressed: () => setState(() => _scale = 22.0),
tooltip: 'إعادة ضبط المحاور',
),
],
),
),
),
// Curves Legend Badge Widget _buildCanvasContainer(List<Offset> points) {
Positioned( return Container(
bottom: 14, margin: const EdgeInsets.all(12),
right: 14, decoration: BoxDecoration(
child: Container( color: const Color(0xFF070C16),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), borderRadius: BorderRadius.circular(20),
decoration: BoxDecoration( border: Border.all(color: AppColors.darkCardBorder),
color: Colors.black.withAlpha(200), boxShadow: const [
borderRadius: BorderRadius.circular(12), BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 8)),
border: Border.all(color: Colors.white12), ],
), ),
child: Column( child: ClipRRect(
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(20),
mainAxisSize: MainAxisSize.min, child: Stack(
children: [ children: [
Row( Positioned.fill(
children: [ child: CustomPaint(
Container(width: 12, height: 3, color: AppColors.saqelCyan), painter: _CartesianCanvasPainter(
const SizedBox(width: 8), curve1Type: _curve1Type,
Text( radiusSquared: _radiusSquared,
_curve1Type == 0 a1: _a1,
? 'الدائرة: x² + y² = ${_radiusSquared.toStringAsFixed(1)}' c1: _c1,
: 'القطع المكافئ 1: y = ${_a1.toStringAsFixed(1)}x² + ${_c1.toStringAsFixed(1)}', curve2Type: _curve2Type,
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600), m: _m,
), b: _b,
], c2: _c2,
), scale: _scale,
const SizedBox(height: 4), intersectionPoints: points,
Row( ),
children: [
Container(width: 12, height: 3, color: AppColors.guardianAmber),
const SizedBox(width: 8),
Text(
_curve2Type == 0
? 'المستقيم: y = ${_m.toStringAsFixed(1)}x + ${_b.toStringAsFixed(1)}'
: 'القطع المكافئ 2: y = x² + ${_c2.toStringAsFixed(1)}',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(color: Color(0xFFFF2D55), shape: BoxShape.circle),
),
const SizedBox(width: 8),
Text(
'نقاط التقاطع: ${points.isEmpty ? "لا يوجد حل حقيقي (∅)" : points.map((p) => "(${p.dx.toStringAsFixed(1)}, ${p.dy.toStringAsFixed(1)})").join(" , ")}',
style: const TextStyle(color: Color(0xFFFF2D55), fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
],
),
),
),
],
), ),
), ),
),
),
// Right: Sliders & Controls Panel // Controls Overlay (Zoom)
Expanded( Positioned(
flex: 4, top: 10,
child: Container( left: 10,
margin: const EdgeInsets.only(top: 16, bottom: 16, left: 16), child: Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.darkSurface, color: Colors.black.withAlpha(180),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.darkCardBorder), border: Border.all(color: Colors.white24),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(CupertinoIcons.zoom_in, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale + 4).clamp(10.0, 50.0)),
tooltip: 'تكبير',
),
IconButton(
icon: const Icon(CupertinoIcons.zoom_out, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale - 4).clamp(10.0, 50.0)),
tooltip: 'تصغير',
),
IconButton(
icon: const Icon(CupertinoIcons.refresh, color: AppColors.saqelCyan, size: 18),
onPressed: () => setState(() => _scale = 22.0),
tooltip: 'إعادة ضبط المحاور',
),
],
),
),
), ),
child: SingleChildScrollView(
child: Column( // Curves Legend Badge
crossAxisAlignment: CrossAxisAlignment.stretch, Positioned(
bottom: 10,
right: 10,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withAlpha(210),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(width: 10, height: 3, color: AppColors.saqelCyan),
const SizedBox(width: 6),
Text(
_curve1Type == 0
? 'الدائرة: x² + y² = ${_radiusSquared.toStringAsFixed(1)}'
: 'القطع المكافئ 1: y = ${_a1.toStringAsFixed(1)}x² + ${_c1.toStringAsFixed(1)}',
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 3),
Row(
children: [
Container(width: 10, height: 3, color: AppColors.guardianAmber),
const SizedBox(width: 6),
Text(
_curve2Type == 0
? 'المستقيم: y = ${_m.toStringAsFixed(1)}x + ${_b.toStringAsFixed(1)}'
: 'القطع المكافئ 2: y = x² + ${_c2.toStringAsFixed(1)}',
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 3),
Row(
children: [
Container(
width: 7,
height: 7,
decoration: const BoxDecoration(color: Color(0xFFFF2D55), shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
'نقاط التقاطع: ${points.isEmpty ? "لا يوجد حل حقيقي (∅)" : points.map((p) => "(${p.dx.toStringAsFixed(1)}, ${p.dy.toStringAsFixed(1)})").join(" , ")}',
style: const TextStyle(color: Color(0xFFFF2D55), fontSize: 10.5, fontWeight: FontWeight.w700),
),
],
),
],
),
),
),
],
),
),
);
}
Widget _buildControlsPanel() {
return Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'لوحة المعاملات الحية 🎛️',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
),
const SizedBox(height: 4),
const Text(
'غيّر المعاملات وشاهد كيف تتحرك المنحنيات وتتغير نقاط التقاطع فورياً',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
const Divider(color: AppColors.darkCardBorder, height: 20),
// Curve 1 controls
Text(
_curve1Type == 0 ? 'معاملات الدائرة (r²)' : 'معاملات المنحنى الأول',
style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 12),
),
if (_curve1Type == 0) ...[
Row(
children: [
const Text('نصف القطر التربيعي (r²):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_radiusSquared.toStringAsFixed(1), style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _radiusSquared,
min: 1.0,
max: 25.0,
divisions: 24,
activeColor: AppColors.saqelCyan,
onChanged: (val) => setState(() => _radiusSquared = val),
),
],
const SizedBox(height: 8),
// Curve 2 controls
Text(
_curve2Type == 0 ? 'معاملات المستقيم (الميل والمقطع)' : 'معاملات القطع المكافئ (الإزاحة الرأسية)',
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 12),
),
if (_curve2Type == 0) ...[
Row(
children: [
const Text('الميل (m):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_m.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _m,
min: -5.0,
max: 5.0,
divisions: 20,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _m = val),
),
Row(
children: [
const Text('المقطع الصادي (b):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_b.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _b,
min: -10.0,
max: 10.0,
divisions: 40,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _b = val),
),
] else ...[
Row(
children: [
const Text('الإزاحة الرأسية (c):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_c2.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _c2,
min: -12.0,
max: 6.0,
divisions: 36,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _c2 = val),
),
],
const SizedBox(height: 12),
// Quick Textbook Presets Buttons
Builder(
builder: (context) {
final activeLessons = _curriculumUnits[_selectedUnitIndex]['lessons'] as List<Map<String, dynamic>>;
final activeLesson = activeLessons[_selectedLessonIndex];
final lessonExercises = activeLesson['exercises'] as List<int>;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text(
'لوحة المعاملات الحية 🎛️',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
),
const SizedBox(height: 4),
const Text(
'غيّر المعاملات وشاهد كيف تتحرك المنحنيات وتتغير نقاط التقاطع فورياً',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
const Divider(color: AppColors.darkCardBorder, height: 24),
// Curve 1 controls
Text( Text(
_curve1Type == 0 ? 'معاملات الدائرة (r²)' : 'معاملات المنحنى الأول', 'تدريبات (${activeLesson['title']}):',
style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 12), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12),
), ),
if (_curve1Type == 0) ...[ const SizedBox(height: 8),
Row( Wrap(
children: [ spacing: 6,
const Text('نصف القطر التربيعي (r²):', style: TextStyle(color: Colors.white70, fontSize: 11)), runSpacing: 6,
const Spacer(), children: lessonExercises.map((i) {
Text(_radiusSquared.toStringAsFixed(1), style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)), final isSelected = _selectedExerciseIndex == i;
], return ChoiceChip(
), label: Text(
Slider( i == 0 ? 'نشاط ص16' : 'تدريب $i',
value: _radiusSquared, style: TextStyle(
min: 1.0, color: isSelected ? Colors.black : Colors.white,
max: 25.0, fontSize: 11,
divisions: 24, fontWeight: FontWeight.w700,
activeColor: AppColors.saqelCyan,
onChanged: (val) => setState(() => _radiusSquared = val),
),
],
const SizedBox(height: 12),
// Curve 2 controls
Text(
_curve2Type == 0 ? 'معاملات المستقيم (الميل والمقطع)' : 'معاملات القطع المكافئ (الإزاحة الرأسية)',
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 12),
),
if (_curve2Type == 0) ...[
Row(
children: [
const Text('الميل (m):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_m.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _m,
min: -5.0,
max: 5.0,
divisions: 20,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _m = val),
),
Row(
children: [
const Text('المقطع الصادي (b):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_b.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _b,
min: -10.0,
max: 10.0,
divisions: 40,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _b = val),
),
] else ...[
Row(
children: [
const Text('الإزاحة الرأسية (c):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_c2.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _c2,
min: -12.0,
max: 6.0,
divisions: 36,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _c2 = val),
),
],
const SizedBox(height: 16),
// Quick Textbook Presets Buttons
Builder(
builder: (context) {
final activeLessons = _curriculumUnits[_selectedUnitIndex]['lessons'] as List<Map<String, dynamic>>;
final activeLesson = activeLessons[_selectedLessonIndex];
final lessonExercises = activeLesson['exercises'] as List<int>;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تدريبات (${activeLesson['title']}):',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12),
), ),
const SizedBox(height: 8), ),
Wrap( selected: isSelected,
spacing: 6, selectedColor: AppColors.saqelCyan,
runSpacing: 6, backgroundColor: AppColors.darkCard,
children: lessonExercises.map((i) { onSelected: (_) => _applyTextbookPreset(i),
final isSelected = _selectedExerciseIndex == i;
return ChoiceChip(
label: Text(
i == 0 ? 'نشاط ص16' : 'تدريب $i',
style: TextStyle(
color: isSelected ? Colors.black : Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
selected: isSelected,
selectedColor: AppColors.saqelCyan,
backgroundColor: AppColors.darkCard,
onSelected: (_) => _applyTextbookPreset(i),
);
}).toList(),
),
],
); );
}, }).toList(),
), ),
], ],
), );
), },
), ),
), ],
], ),
); );
} }
@@ -1001,18 +1024,20 @@ class _CartesianCanvasPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (size.width <= 0 || size.height <= 0) return;
final cx = size.width / 2; final cx = size.width / 2;
final cy = size.height / 2; final cy = size.height / 2;
final safeScale = scale.clamp(6.0, 60.0);
// 1. Draw Grid Lines // 1. Draw Grid Lines
final gridPaint = Paint() final gridPaint = Paint()
..color = const Color(0xFF1B263B) ..color = const Color(0xFF1B263B)
..strokeWidth = 0.8; ..strokeWidth = 0.8;
for (double x = cx % scale; x < size.width; x += scale) { for (double x = cx % safeScale; x < size.width; x += safeScale) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint); canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
} }
for (double y = cy % scale; y < size.height; y += scale) { for (double y = cy % safeScale; y < size.height; y += safeScale) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint); canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
} }
@@ -1032,16 +1057,20 @@ class _CartesianCanvasPainter extends CustomPainter {
if (curve1Type == 0) { if (curve1Type == 0) {
// Circle: x² + y² = r² // Circle: x² + y² = r²
final rPixel = math.sqrt(radiusSquared) * scale; final safeR2 = radiusSquared > 0 ? radiusSquared : 0.0;
canvas.drawCircle(Offset(cx, cy), rPixel, curve1Paint); final rPixel = math.sqrt(safeR2) * safeScale;
if (rPixel.isFinite && rPixel > 0) {
canvas.drawCircle(Offset(cx, cy), rPixel, curve1Paint);
}
} else { } else {
// Parabola: y = a1*x² + c1 // Parabola: y = a1*x² + c1
final path = Path(); final path = Path();
bool started = false; bool started = false;
for (double px = 0; px <= size.width; px += 2) { for (double px = 0; px <= size.width; px += 2) {
final mathX = (px - cx) / scale; final mathX = (px - cx) / safeScale;
final mathY = a1 * mathX * mathX + c1; final mathY = a1 * mathX * mathX + c1;
final py = cy - mathY * scale; final py = cy - mathY * safeScale;
if (!py.isFinite) continue;
if (!started) { if (!started) {
path.moveTo(px, py); path.moveTo(px, py);
started = true; started = true;
@@ -1049,7 +1078,9 @@ class _CartesianCanvasPainter extends CustomPainter {
path.lineTo(px, py); path.lineTo(px, py);
} }
} }
canvas.drawPath(path, curve1Paint); if (started) {
canvas.drawPath(path, curve1Paint);
}
} }
// 4. Draw Curve 2 (Amber) // 4. Draw Curve 2 (Amber)
@@ -1060,23 +1091,26 @@ class _CartesianCanvasPainter extends CustomPainter {
if (curve2Type == 0) { if (curve2Type == 0) {
// Line: y = m*x + b // Line: y = m*x + b
final x1 = (0 - cx) / scale; final x1 = (0 - cx) / safeScale;
final y1 = m * x1 + b; final y1 = m * x1 + b;
final py1 = cy - y1 * scale; final py1 = cy - y1 * safeScale;
final x2 = (size.width - cx) / scale; final x2 = (size.width - cx) / safeScale;
final y2 = m * x2 + b; final y2 = m * x2 + b;
final py2 = cy - y2 * scale; final py2 = cy - y2 * safeScale;
canvas.drawLine(Offset(0, py1), Offset(size.width, py2), curve2Paint); if (py1.isFinite && py2.isFinite) {
canvas.drawLine(Offset(0, py1), Offset(size.width, py2), curve2Paint);
}
} else { } else {
// Parabola: y = x² + c2 // Parabola: y = x² + c2
final path = Path(); final path = Path();
bool started = false; bool started = false;
for (double px = 0; px <= size.width; px += 2) { for (double px = 0; px <= size.width; px += 2) {
final mathX = (px - cx) / scale; final mathX = (px - cx) / safeScale;
final mathY = mathX * mathX + c2; final mathY = mathX * mathX + c2;
final py = cy - mathY * scale; final py = cy - mathY * safeScale;
if (!py.isFinite) continue;
if (!started) { if (!started) {
path.moveTo(px, py); path.moveTo(px, py);
started = true; started = true;
@@ -1084,7 +1118,9 @@ class _CartesianCanvasPainter extends CustomPainter {
path.lineTo(px, py); path.lineTo(px, py);
} }
} }
canvas.drawPath(path, curve2Paint); if (started) {
canvas.drawPath(path, curve2Paint);
}
} }
// 5. Draw Intersection Points (Red Glow) // 5. Draw Intersection Points (Red Glow)
@@ -1097,8 +1133,10 @@ class _CartesianCanvasPainter extends CustomPainter {
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
for (var pt in intersectionPoints) { for (var pt in intersectionPoints) {
final px = cx + pt.dx * scale; if (!pt.dx.isFinite || !pt.dy.isFinite) continue;
final py = cy - pt.dy * scale; final px = cx + pt.dx * safeScale;
final py = cy - pt.dy * safeScale;
if (!px.isFinite || !py.isFinite) continue;
canvas.drawCircle(Offset(px, py), 9.0, ptGlow); canvas.drawCircle(Offset(px, py), 9.0, ptGlow);
canvas.drawCircle(Offset(px, py), 5.0, ptPaint); canvas.drawCircle(Offset(px, py), 5.0, ptPaint);
File diff suppressed because it is too large Load Diff
@@ -12,6 +12,7 @@ import '../../../logic/cubits/dashboard_cubits.dart';
import '../../widgets/luxury_widgets.dart'; import '../../widgets/luxury_widgets.dart';
import '../curriculum/subjects_grid_screen.dart'; import '../curriculum/subjects_grid_screen.dart';
import '../notebook/smart_error_notebook_screen.dart'; import '../notebook/smart_error_notebook_screen.dart';
import '../vocational/vocational_training_screen.dart';
class UnifiedHomeScreen extends StatefulWidget { class UnifiedHomeScreen extends StatefulWidget {
final UserModel user; final UserModel user;
@@ -319,6 +320,71 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Vocational Training Corporation (VTC) & EV Lab Gateway
InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const VocationalTrainingScreen()),
);
},
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF064E3B), Color(0xFF022C22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF10B981).withAlpha(160), width: 1.5),
boxShadow: [
BoxShadow(
color: const Color(0xFF10B981).withAlpha(35),
blurRadius: 18,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF10B981), Color(0xFF059669)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(CupertinoIcons.wrench_fill, color: Colors.black, size: 24),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'مؤسسة التدريب المهني (VTC Hub) ⚡',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 14.5),
),
SizedBox(height: 4),
Text(
'140 مهنة • 8 قطاعات صناعية • مختبر فحص المركبات الكهربائية (EV)',
style: TextStyle(color: Color(0xFF6EE7B7), fontSize: 11.5, fontWeight: FontWeight.w600),
),
],
),
),
const Icon(CupertinoIcons.chevron_back, color: Color(0xFF10B981), size: 18),
],
),
),
),
const SizedBox(height: 12),
// Smart Error Notebook & Remediation Pathway Gateway // Smart Error Notebook & Remediation Pathway Gateway
InkWell( InkWell(
onTap: () { onTap: () {
@@ -0,0 +1,934 @@
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../widgets/luxury_widgets.dart';
/// ==============================================================================
/// SAQEL ENTERPRISE (EDTECH 2.0) - VOCATIONAL TRAINING CORPORATION (VTC) HUB
/// ==============================================================================
///
/// بوابة منظومة مؤسسة التدريب المهني (VTC) والتعليم التقني والتدريب المهني:
/// 1. دليل الـ 140 مهنة وحرفة موزعة على 8 قطاعات إنتاجية وصناعية وطنية.
/// 2. تخصص صيانة وتشخيص المركبات الكهربائية والهجينة (EV & Hybrid Specialist).
/// 3. محاكي فحص بطاريات الجهد العالي ونظام العزل (HVIL & BMS 120 FPS Lab).
/// 4. نظام الكفايات المهنية (CBT) ومعايير السلامة المهنية للمركبات الكهربائية (NFPA 70E).
class VocationalTrainingScreen extends StatefulWidget {
const VocationalTrainingScreen({super.key});
@override
State<VocationalTrainingScreen> createState() => _VocationalTrainingScreenState();
}
class _VocationalTrainingScreenState extends State<VocationalTrainingScreen>
with SingleTickerProviderStateMixin {
int _selectedSectorIndex = 0;
int _activeTabIndex = 0; // 0 = 140 Trades Directory, 1 = EV Diagnostic Lab, 2 = CBT Modules
// EV Lab Parameters
double _batterySoc = 82.0; // State of Charge %
double _packVoltage = 384.0; // Volts DC
double _isolationResistance = 5.2; // Mega-Ohms (Threshold > 0.5 MΩ)
bool _hvilInterlockClosed = true; // High Voltage Interlock Loop
bool _contactorPrechargeActive = true;
String _activeDtcCode = 'P0000: No Faults Detected (Normal Operation)';
bool _simulationRunning = true;
late final AnimationController _pulseController;
final List<Map<String, dynamic>> _sectors = [
{
'title': 'المركبات والطاقة المتجددة',
'icon': CupertinoIcons.bolt_car,
'color': Color(0xFF10B981),
'tradesCount': 18,
'featured': 'ميكانيك وتشخيص المركبات الكهربائية والهجينة (EV)',
'description': 'فحص أنظمة الجهد العالي، بطاريات الليثيوم، ومحولات التيار (Inverters).',
'trades': [
'تشخيص وصيانة المركبات الكهربائية والهجينة',
'كهرباء وإلكترونيات السيارات الحديثة',
'ميكانيك محركات الديزل والبنزين المتقدم',
'تركيب وصيانة أنظمة الطاقة الشمسية الكهروضوئية',
'صيانة طواحين الرياح وأنظمة التوليد الهجينة',
'فحص بطاريات الجهد العالي وإعادة تدوير الخلايا',
],
},
{
'title': 'التكنولوجيا والتحول الرقمي',
'icon': CupertinoIcons.device_laptop,
'color': Color(0xFF00F5D4),
'tradesCount': 22,
'featured': 'أمن الشبكات والدعم الفني السحابي',
'description': 'هندسة الشبكات المحلية، الألياف الضوئية، وصيانة الخوادم والأنظمة الذكية.',
'trades': [
'فني صيانة شبكات الحاسوب والألياف الضوئية',
'الدعم الفني لأنظمة الحوسبة السحابية',
'فني صيانة الهواتف الذكية والأجهزة اللوحية',
'تشغيل وبرمجة أنظمة التحكم الرقمي (CNC)',
'فني تركيب كاميرات المراقبة وأنظمة الأمان الذكية',
],
},
{
'title': 'الصناعات الهندسية والميكانيكية',
'icon': CupertinoIcons.wrench_fill,
'color': Color(0xFFF59E0B),
'tradesCount': 28,
'featured': 'اللحام وتشكيل المعادن المتقدم (TIG/MIG)',
'description': 'الخراطة، تشكيل المعادن، واللحام بالأرغون وخطوط الإنتاج المؤتمتة.',
'trades': [
'لحام الأنابيب الصناعية وضغط الغاز (TIG/MIG)',
'الخراطة والتفريز وتشكيل المعادن المبرمج',
'صيانة خطوط الإنتاج والآلات الصناعية',
'التمديدات الكهروميكانيكية للمصانع',
'تشكيل وهيكلة الألمنيوم والواجهات المعمارية',
],
},
{
'title': 'الضيافة والسياحة والفندقة',
'icon': CupertinoIcons.building_2_fill,
'color': Color(0xFFEC4899),
'tradesCount': 16,
'featured': 'فنون الطهي وإنتاج الأغذية الفندقية',
'description': 'إدارة المكاتب الأمامية، فنون الطهي الدولي، وخدمة الغرف والضيافة.',
'trades': [
'إنتاج الأغذية والطهي الشرقي والغربي',
'فنون الحلويات والمخبوزات الفرنسية',
'إدارة المطاعم وخدمة الضيوف الراقية',
'الإشراف الداخلي وإدارة الغرف الفندقية',
],
},
{
'title': 'الزراعة الحديثة والري الذكي',
'icon': CupertinoIcons.leaf_arrow_circlepath,
'color': Color(0xFF22C55E),
'tradesCount': 14,
'featured': 'الزراعة المائية والبيوت البلاستيكية الذكية',
'description': 'أنظمة الزراعة بدون تربة (Hydroponics)، الري المحوسب، وتنسيق الحدائق.',
'trades': [
'تقنيات الزراعة المائية والعمودية المغلقة',
'إدارة وصيانة شبكات الري بالتنقيط المؤتمتة',
'إنتاج النباتات الطبية والعطرية العضوية',
'تنسيق وتصميم الحدائق والمتنزهات الخضراء',
],
},
{
'title': 'الصناعات الدوائية والكيميائية',
'icon': CupertinoIcons.lab_flask_solid,
'color': Color(0xFF8B5CF6),
'tradesCount': 12,
'featured': 'تشغيل خطوط التعبئة والتغليف الدوائي',
'description': 'تطبيق معايير ممارسات التصنيع الجيد (GMP) ومراقبة جودة الإنتاج الكيميائي.',
'trades': [
'فني تشغيل آلات التصنيع الدوائي المعقم',
'مساعد مراقبة الجودة والتحاليل الكيميائية',
'صيانة المضخات والمفاعلات الكيميائية',
],
},
{
'title': 'الحلاقة والتجميل والعناية',
'icon': CupertinoIcons.scissors,
'color': Color(0xFFF43F5E),
'tradesCount': 15,
'featured': 'تصفيف الشعر والتجميل الاحترافي',
'description': 'تقنيات العناية بالبشرة، التصميم المسرحي، وإدارة الصالونات الحديثة.',
'trades': [
'الحلاقة وتصفيف الشعر الرجالي المتقدم',
'التجميل وتصفيف الشعر النسائي',
'العناية بالبشرة والمساج العلاجي',
],
},
{
'title': 'الحرف التقليدية والمشغولات اليدوية',
'icon': CupertinoIcons.cube_box_fill,
'color': Color(0xFFD97706),
'tradesCount': 15,
'featured': 'صياغة الذهب والمجوهرات والفسيفساء',
'description': 'إحياء التراث الحرفي الأردني، فن الفسيفساء المادبي، وحفر الخشب التراثي.',
'trades': [
'صياغة الحلي والمجوهرات وتركيب الأحجار الكريمة',
'فن الفسيفساء والترميم الأثري الحرفي',
'النجارة التراثية والحفر على الخشب والزخرفة',
'الخزف والفخار الفني والتشكيل اليدوي',
],
},
];
@override
void initState() {
super.initState();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
}
@override
void dispose() {
_pulseController.dispose();
super.dispose();
}
void _triggerFault(String faultCode, String description, double rIso, bool hvilState) {
setState(() {
_activeDtcCode = '$faultCode: $description';
_isolationResistance = rIso;
_hvilInterlockClosed = hvilState;
if (!hvilState || rIso < 0.5) {
_contactorPrechargeActive = false;
}
});
}
void _clearFaults() {
setState(() {
_activeDtcCode = 'P0000: No Faults Detected (Normal Operation)';
_isolationResistance = 5.2;
_hvilInterlockClosed = true;
_contactorPrechargeActive = true;
});
}
@override
Widget build(BuildContext context) {
final activeSector = _sectors[_selectedSectorIndex];
return Scaffold(
backgroundColor: AppColors.darkBackground,
appBar: AppBar(
backgroundColor: const Color(0xFF091620),
elevation: 0,
title: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withAlpha(30),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF10B981).withAlpha(100)),
),
child: const Icon(CupertinoIcons.wrench_fill, color: Color(0xFF10B981), size: 18),
),
const SizedBox(width: 10),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'مؤسسة التدريب المهني (VTC Hub)',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
),
Text(
'140 مهنة وحرفة معتمدة • الشراكة الاستراتيجية للتحول المهني',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
],
),
],
),
),
body: Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: [
// Top Executive Stats Strip
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
color: Color(0xFF0A1E1D),
border: Border(bottom: BorderSide(color: Color(0xFF10B981), width: 0.8)),
),
child: Row(
children: [
_buildExecutiveStat('140 مهنة', 'دليل المهن الوزاري', CupertinoIcons.square_grid_2x2_fill),
_buildVerticalDivider(),
_buildExecutiveStat('8 قطاعات', 'سلاسل القيمة الصناعية', CupertinoIcons.layers_fill),
_buildVerticalDivider(),
_buildExecutiveStat('EV 384V', 'مختبر فحص الهايبرد والكهرباء', CupertinoIcons.bolt_car_fill),
_buildVerticalDivider(),
_buildExecutiveStat('CBT Level 3', 'الكفايات والسلامة الدولية', CupertinoIcons.checkmark_seal_fill),
],
),
),
// Navigation Tabs (Directory / EV Lab / CBT)
Container(
color: AppColors.darkSurface,
child: Row(
children: [
_buildTopTabItem(0, 'دليل الـ 140 مهنة (8 قطاعات) 📋', CupertinoIcons.list_bullet),
_buildTopTabItem(1, 'مختبر فحص المركبات الكهربائية ⚡', CupertinoIcons.bolt_fill),
_buildTopTabItem(2, 'شهادات الكفايات المهنية (CBT) 🎓', CupertinoIcons.rosette),
],
),
),
// Tab View Body
Expanded(
child: IndexedStack(
index: _activeTabIndex,
children: [
_buildTradesDirectoryTab(activeSector),
_buildEvDiagnosticLabTab(),
_buildCbtCompetenciesTab(),
],
),
),
],
),
),
);
}
Widget _buildTopTabItem(int index, String label, IconData icon) {
final isSelected = _activeTabIndex == index;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _activeTabIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelected ? const Color(0xFF10B981) : Colors.transparent,
width: 3,
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: isSelected ? const Color(0xFF10B981) : AppColors.textSecondaryDark),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : AppColors.textSecondaryDark,
fontSize: 11.5,
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
Widget _buildExecutiveStat(String title, String subtitle, IconData icon) {
return Expanded(
child: Row(
children: [
Icon(icon, color: const Color(0xFF10B981), size: 18),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 12.5)),
Text(subtitle, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 9.5), maxLines: 1, overflow: TextOverflow.ellipsis),
],
),
),
],
),
);
}
Widget _buildVerticalDivider() {
return Container(
width: 1,
height: 24,
margin: const EdgeInsets.symmetric(horizontal: 8),
color: Colors.white12,
);
}
// ===========================================================================
// TAB 1: 140 VOCATIONAL TRADES DIRECTORY & 8 SECTORS
// ===========================================================================
Widget _buildTradesDirectoryTab(Map<String, dynamic> activeSector) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Sector Pill Chips Row
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _sectors.asMap().entries.map((entry) {
final idx = entry.key;
final sec = entry.value;
final isSelected = _selectedSectorIndex == idx;
final secColor = sec['color'] as Color;
return GestureDetector(
onTap: () => setState(() => _selectedSectorIndex = idx),
child: Container(
margin: const EdgeInsets.only(left: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? secColor.withAlpha(40) : AppColors.darkSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isSelected ? secColor : AppColors.darkCardBorder,
width: 1.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(sec['icon'] as IconData, size: 15, color: isSelected ? secColor : Colors.white70),
const SizedBox(width: 6),
Text(
sec['title'] as String,
style: TextStyle(
color: isSelected ? Colors.white : Colors.white70,
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500,
fontSize: 12,
),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: secColor.withAlpha(60),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${sec['tradesCount']}',
style: TextStyle(color: secColor, fontSize: 10, fontWeight: FontWeight.w900),
),
),
],
),
),
);
}).toList(),
),
),
const SizedBox(height: 16),
// Featured Specialization Spotlight Card
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
(activeSector['color'] as Color).withAlpha(40),
const Color(0xFF0F1A28),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: (activeSector['color'] as Color).withAlpha(120), width: 1.5),
boxShadow: const [
BoxShadow(color: Colors.black45, blurRadius: 16, offset: Offset(0, 6)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: (activeSector['color'] as Color).withAlpha(40),
borderRadius: BorderRadius.circular(12),
),
child: Icon(activeSector['icon'] as IconData, color: activeSector['color'] as Color, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activeSector['title'] as String,
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w800),
),
const SizedBox(height: 2),
Text(
'التخصص الاستراتيجي: ${activeSector['featured']}',
style: TextStyle(color: activeSector['color'] as Color, fontSize: 12, fontWeight: FontWeight.w700),
),
],
),
),
],
),
const SizedBox(height: 12),
Text(
activeSector['description'] as String,
style: const TextStyle(color: Colors.white70, fontSize: 12.5, height: 1.5),
),
],
),
),
const SizedBox(height: 16),
// Trades List
const Text(
'المهن والحرف المعتمدة في هذا القطاع:',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
),
const SizedBox(height: 10),
...((activeSector['trades'] as List<String>).asMap().entries.map((tEntry) {
final idx = tEntry.key + 1;
final tradeTitle = tEntry.value;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Colors.white.withAlpha(10),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'$idx',
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
tradeTitle,
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withAlpha(20),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF10B981).withAlpha(80)),
),
child: const Text('دبلوم مهني VTC', style: TextStyle(color: Color(0xFF10B981), fontSize: 10, fontWeight: FontWeight.bold)),
),
],
),
);
}).toList()),
],
);
}
// ===========================================================================
// TAB 2: INTERACTIVE EV & HYBRID DIAGNOSTIC VIRTUAL LAB (120 FPS)
// ===========================================================================
Widget _buildEvDiagnosticLabTab() {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Live Diagnostics Canvas (Battery Pack & Inverter)
Container(
height: 280,
decoration: BoxDecoration(
color: const Color(0xFF070E14),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981).withAlpha(80) : const Color(0xFFFF2D55),
width: 1.5,
),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 6)),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(19),
child: CustomPaint(
painter: _EvDiagnosticsPainter(
batterySoc: _batterySoc,
packVoltage: _packVoltage,
isolationResistance: _isolationResistance,
hvilClosed: _hvilInterlockClosed,
isFaulted: !_activeDtcCode.startsWith('P0000'),
),
),
),
),
const SizedBox(height: 14),
// Live Diagnostic Telemetry Grid
Row(
children: [
_buildTelemetryCard(
'حالة الشحن (SoC)',
'${_batterySoc.toInt()}%',
const Color(0xFF10B981),
CupertinoIcons.battery_75_percent,
),
const SizedBox(width: 8),
_buildTelemetryCard(
'جهد البطارية (Pack V)',
'${_packVoltage.toInt()} V',
AppColors.saqelCyan,
CupertinoIcons.bolt_fill,
),
const SizedBox(width: 8),
_buildTelemetryCard(
'مقاومة العزل (R_iso)',
'${_isolationResistance.toStringAsFixed(1)} MΩ',
_isolationResistance >= 0.5 ? const Color(0xFF10B981) : const Color(0xFFFF2D55),
CupertinoIcons.shield_lefthalf_fill,
),
const SizedBox(width: 8),
_buildTelemetryCard(
'حلقة القفل (HVIL)',
_hvilInterlockClosed ? 'مغلقة (آمن)' : 'مفتوحة (خطر)',
_hvilInterlockClosed ? const Color(0xFF10B981) : const Color(0xFFFF2D55),
CupertinoIcons.lock_shield_fill,
),
],
),
const SizedBox(height: 14),
// Active DTC Code Box
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF0F261E) : const Color(0xFF330E14),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981) : const Color(0xFFFF2D55),
),
),
child: Row(
children: [
Icon(
_activeDtcCode.startsWith('P0000') ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.exclamationmark_triangle_fill,
color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981) : const Color(0xFFFF2D55),
size: 22,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('كود التشخيص والأعطال (OBD-II DTC Readout):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const SizedBox(height: 2),
Text(
_activeDtcCode,
style: TextStyle(
color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981) : const Color(0xFFFF2D55),
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
],
),
),
],
),
),
const SizedBox(height: 14),
// Simulated Fault Injection Controls
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('حقن الأعطال المخبرية (Fault Injection Simulation):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13)),
const SizedBox(height: 4),
const Text('اختر عطلاً كهربائياً لاختبار استجابة نظام إدارة البطارية (BMS) وفصل قواطع الجهد العالي:', style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11)),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
OutlinedButton.icon(
icon: const Icon(CupertinoIcons.shield_slash, color: Color(0xFFFF2D55), size: 16),
label: const Text('عطل تسريب العزل (P0AA6 - Loss of Isolation)', style: TextStyle(color: Colors.white, fontSize: 11.5)),
style: OutlinedButton.styleFrom(side: const BorderSide(color: Color(0xFFFF2D55))),
onPressed: () => _triggerFault('P0AA6', 'Hybrid Battery Voltage Isolation Fault (Leakage < 0.5 MΩ)', 0.1, true),
),
OutlinedButton.icon(
icon: const Icon(CupertinoIcons.lock_slash_fill, color: Color(0xFFF59E0B), size: 16),
label: const Text('قطع حلقة الأمان (P0A0D - HVIL Open Circuit)', style: TextStyle(color: Colors.white, fontSize: 11.5)),
style: OutlinedButton.styleFrom(side: const BorderSide(color: Color(0xFFF59E0B))),
onPressed: () => _triggerFault('P0A0D', 'High Voltage System Interlock Circuit Open (HVIL Broken)', 5.2, false),
),
ElevatedButton.icon(
icon: const Icon(CupertinoIcons.arrow_counterclockwise, size: 16),
label: const Text('إعادة ضبط وتشغيل النظام السليم (Clear DTC)', style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF10B981), foregroundColor: Colors.black),
onPressed: _clearFaults,
),
],
),
],
),
),
],
);
}
Widget _buildTelemetryCard(String label, String value, Color color, IconData icon) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Column(
children: [
Icon(icon, color: color, size: 18),
const SizedBox(height: 4),
Text(value, style: TextStyle(color: color, fontWeight: FontWeight.w800, fontSize: 13.5)),
const SizedBox(height: 2),
Text(label, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 9.5), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis),
],
),
),
);
}
// ===========================================================================
// TAB 3: CBT COMPETENCIES & CERTIFICATION STANDARDS
// ===========================================================================
Widget _buildCbtCompetenciesTab() {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Level 3 Certification Banner
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF132A1C), Color(0xFF0F1A28)],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF10B981), width: 1.5),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(CupertinoIcons.rosette, color: Color(0xFF10B981), size: 24),
SizedBox(width: 10),
Text(
'نظام التدريب المبني على الكفايات (CBT Level 3)',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 15),
),
],
),
SizedBox(height: 8),
Text(
'برنامج معتمد دولياً لتأهيل فنيي المركبات الكهربائية والهجينة وفق معايير السلامة المهنية العالمية (NFPA 70E و ISO 26262 و ECE-R100). يشترط إتقان 5 كفايات إلزامية لنيل رخصة الفحص الميداني.',
style: TextStyle(color: Colors.white70, fontSize: 12.5, height: 1.5),
),
],
),
),
const SizedBox(height: 16),
_buildCbtModuleCard(
'الكفاية 1: إجراءات السلامة وفصل الجهد العالي (HV De-energization)',
'التحقق من خلو الجهد الكهربائي (Zero Voltage Verification) واستخدام قفازات الفئة Class 0 (1000V).',
'95% إتقان مطلوب',
CupertinoIcons.shield_fill,
const Color(0xFF10B981),
),
_buildCbtModuleCard(
'الكفاية 2: تشخيص خلايا بطارية الجهد العالي (BMS Cell Balancing)',
'قراءة قيم الجهد لكل خلية (3.2V - 4.2V) وفحص مقاومة العزل وتوازن درجات الحرارة.',
'معتمد دولياً',
CupertinoIcons.battery_full,
AppColors.saqelCyan,
),
_buildCbtModuleCard(
'الكفاية 3: فحص محول التيار والمحرك الكهربائي (Inverter & Motor)',
'فحص إشارات التردد العالي (PWM Gate Signals) وقياس مقاومة أطوار المحرك الثلاثية (U-V-W).',
'مستوى متقدم',
CupertinoIcons.bolt_horizontal_fill,
const Color(0xFFF59E0B),
),
_buildCbtModuleCard(
'الكفاية 4: بروتوكول حلقة الأمان وقفل الفحص (HVIL Loop Diagnostics)',
'كشف انقطاع أسلاك التوصيل السريع وإشارات تأكيد إغلاق غطاء مقبس الخدمة (Service Plug).',
'إلزامي للفحص',
CupertinoIcons.lock_shield,
const Color(0xFF8B5CF6),
),
],
);
}
Widget _buildCbtModuleCard(String title, String desc, String badge, IconData icon, Color color) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withAlpha(30),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withAlpha(20),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withAlpha(80)),
),
child: Text(badge, style: TextStyle(color: color, fontSize: 10.5, fontWeight: FontWeight.bold)),
),
],
),
const SizedBox(height: 6),
Text(desc, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12, height: 1.4)),
],
),
),
],
),
);
}
}
// =============================================================================
// EV DIAGNOSTICS CUSTOM PAINTER (120 FPS HIGH-VOLTAGE ARCHITECTURE)
// =============================================================================
class _EvDiagnosticsPainter extends CustomPainter {
final double batterySoc;
final double packVoltage;
final double isolationResistance;
final bool hvilClosed;
final bool isFaulted;
_EvDiagnosticsPainter({
required this.batterySoc,
required this.packVoltage,
required this.isolationResistance,
required this.hvilClosed,
required this.isFaulted,
});
@override
void paint(Canvas canvas, Size size) {
final w = size.width;
final h = size.height;
final cy = h / 2;
// Watermark
const watermarkSpan = TextSpan(
text: 'منصة صَقِل • مختبر فحص المركبات الكهربائية VTC EV Lab',
style: TextStyle(color: Color(0x3300F5D4), fontSize: 11, fontWeight: FontWeight.bold),
);
final watermarkPainter = TextPainter(text: watermarkSpan, textDirection: TextDirection.rtl)..layout();
watermarkPainter.paint(canvas, const Offset(12, 12));
// 1. High Voltage Battery Pack Silhouette
final bRect = RRect.fromRectAndRadius(
Rect.fromLTWH(24, cy - 65, w * 0.40, 130),
const Radius.circular(16),
);
canvas.drawRRect(bRect, Paint()..color = const Color(0xFF0F1E29));
canvas.drawRRect(bRect, Paint()..color = const Color(0xFF10B981)..style = PaintingStyle.stroke..strokeWidth = 2);
// Battery Pack label
const bSpan = TextSpan(
text: 'بطارية الجهد العالي (384V DC)\nLi-Ion NMC Battery Pack',
style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
);
final btp = TextPainter(text: bSpan, textDirection: TextDirection.rtl)..layout();
btp.paint(canvas, Offset(36, cy - 45));
// SoC Bar inside pack
final socW = (w * 0.40 - 24) * (batterySoc / 100.0);
canvas.drawRRect(
RRect.fromRectAndRadius(Rect.fromLTWH(36, cy + 20, socW, 16), const Radius.circular(6)),
Paint()..color = const Color(0xFF10B981),
);
// 2. Inverter & Motor Unit
final invRect = RRect.fromRectAndRadius(
Rect.fromLTWH(w * 0.60, cy - 65, w * 0.34, 130),
const Radius.circular(16),
);
canvas.drawRRect(invRect, Paint()..color = const Color(0xFF1E1A2E));
canvas.drawRRect(
invRect,
Paint()
..color = isFaulted ? const Color(0xFFFF2D55) : const Color(0xFF8B5CF6)
..style = PaintingStyle.stroke
..strokeWidth = 2,
);
final invSpan = TextSpan(
text: 'محول القدرة والمحرك (Inverter)\nAC 3-Phase Permanent Magnet',
style: TextStyle(color: isFaulted ? const Color(0xFFFF2D55) : Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
);
final itp = TextPainter(text: invSpan, textDirection: TextDirection.rtl)..layout();
itp.paint(canvas, Offset(w * 0.60 + 12, cy - 45));
// 3. High-Voltage Bus Cables (Orange Cables +HV and -HV)
final hvCableColor = isFaulted ? const Color(0xFFFF2D55) : const Color(0xFFFF7A00); // Standard Orange EV Cable
final cablePaint = Paint()..color = hvCableColor..strokeWidth = 4..strokeCap = StrokeCap.round;
// +HV Cable (Top)
canvas.drawLine(Offset(24 + w * 0.40, cy - 20), Offset(w * 0.60, cy - 20), cablePaint);
// -HV Cable (Bottom)
canvas.drawLine(Offset(24 + w * 0.40, cy + 20), Offset(w * 0.60, cy + 20), cablePaint);
// Cable Voltage Badge
final voltSpan = TextSpan(
text: isFaulted ? '⚠️ جهد معزول تلقائياً' : '+384V DC Bus',
style: TextStyle(color: hvCableColor, fontSize: 10, fontWeight: FontWeight.w800),
);
final vtp = TextPainter(text: voltSpan, textDirection: TextDirection.ltr)..layout();
vtp.paint(canvas, Offset(w * 0.46, cy - 36));
// 4. HVIL Interlock Loop Indicator
final hvilColor = hvilClosed ? const Color(0xFF10B981) : const Color(0xFFFF2D55);
final hvilPaint = Paint()..color = hvilColor..strokeWidth = 2..style = PaintingStyle.stroke;
canvas.drawCircle(Offset(w * 0.50, cy + 45), 8, hvilPaint);
canvas.drawCircle(Offset(w * 0.50, cy + 45), 4, Paint()..color = hvilColor);
}
@override
bool shouldRepaint(covariant _EvDiagnosticsPainter oldDelegate) => true;
}