Update Saqel Platform: 2026-09-05 22:17:15
This commit is contained in:
@@ -49,30 +49,49 @@ class EnglishReadingService {
|
||||
if (_isInitialized) return;
|
||||
|
||||
try {
|
||||
await _tts.setLanguage('en-US');
|
||||
await _tts.setSpeechRate(0.44); // Standard natural reading speed for learners
|
||||
await _tts.setLanguage('en-GB');
|
||||
await _tts.setSpeechRate(0.40); // British English natural learner cadence
|
||||
await _tts.setVolume(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 {
|
||||
final voices = await _tts.getVoices;
|
||||
if (voices is List) {
|
||||
Map? preferredVoice;
|
||||
Map? fallbackGbVoice;
|
||||
|
||||
for (var v in voices) {
|
||||
if (v is Map) {
|
||||
final name = (v['name'] ?? '').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'))) {
|
||||
await _tts.setVoice({'name': v['name'], 'locale': v['locale']});
|
||||
AppLogger.log('Selected high-quality natural voice: ${v['name']}', tag: 'ENGLISH_AUDIO');
|
||||
|
||||
if (locale.contains('en-gb') || locale.contains('en_gb')) {
|
||||
fallbackGbVoice ??= v;
|
||||
if (name.contains('natural') ||
|
||||
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) {
|
||||
AppLogger.log('Voice auto-tuning warning: $e', tag: 'ENGLISH_AUDIO');
|
||||
AppLogger.log('British voice auto-tuning warning: $e', tag: 'ENGLISH_AUDIO');
|
||||
}
|
||||
|
||||
_tts.setStartHandler(() {
|
||||
@@ -104,7 +123,7 @@ class EnglishReadingService {
|
||||
}
|
||||
}
|
||||
|
||||
/// قراءة فقرة محددة مع إيقاف أي قراءة جارية
|
||||
/// قراءة فقرة محددة بالنموذج البريطاني النقي مع إيقاف أي قراءة جارية
|
||||
Future<void> readParagraph({
|
||||
required String paragraphId,
|
||||
required String text,
|
||||
@@ -134,11 +153,12 @@ class EnglishReadingService {
|
||||
return;
|
||||
}
|
||||
|
||||
await _tts.setSpeechRate(0.44);
|
||||
await _tts.setLanguage('en-GB');
|
||||
await _tts.setSpeechRate(0.40);
|
||||
await _tts.speak(cleanText);
|
||||
}
|
||||
|
||||
/// نطق كلمة واحدة مفردة بوضوح صوتي نقي
|
||||
/// نطق كلمة واحدة مفردة بمخارج حروف دقيقة جداً (British RP Articulation)
|
||||
Future<void> speakWord(String word) async {
|
||||
await initialize();
|
||||
if (word.trim().contains(' ')) {
|
||||
@@ -149,11 +169,12 @@ class EnglishReadingService {
|
||||
if (cleanWord.isEmpty) return;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// نطق جملة أو عبارة تعليمية بوضوح وصوت طبيعي
|
||||
/// نطق جملة أو عبارة تعليمية باللكنة البريطانية المعتمدة
|
||||
Future<void> speakSentence(String text) async {
|
||||
await initialize();
|
||||
final cleanText = text
|
||||
@@ -163,7 +184,8 @@ class EnglishReadingService {
|
||||
if (cleanText.isEmpty) return;
|
||||
|
||||
await _tts.stop();
|
||||
await _tts.setSpeechRate(0.44);
|
||||
await _tts.setLanguage('en-GB');
|
||||
await _tts.setSpeechRate(0.40);
|
||||
await _tts.speak(cleanText);
|
||||
}
|
||||
|
||||
|
||||
+130
-19
@@ -532,18 +532,18 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.appleBlue.withAlpha(30),
|
||||
color: const Color(0xFF1E3A8A).withAlpha(140),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.appleBlue.withAlpha(90)),
|
||||
border: Border.all(color: const Color(0xFF60A5FA).withAlpha(160)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.speaker_2_fill, color: AppColors.appleBlue, size: 12),
|
||||
SizedBox(width: 4),
|
||||
Text('🇬🇧', style: TextStyle(fontSize: 12)),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
'صوتيات ذكية (IPA & TTS)',
|
||||
style: TextStyle(color: AppColors.appleBlue, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
'صوت بريطاني فصيح (en-GB RP)',
|
||||
style: TextStyle(color: Color(0xFF93C5FD), fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -929,11 +929,90 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
// ============================================================================
|
||||
// TAB 2: PHONETICS & PRONUNCIATION STUDIO (IPA & TTS)
|
||||
// ============================================================================
|
||||
// ============================================================================
|
||||
// TAB 2: PHONETICS & PRONUNCIATION STUDIO (BRITISH RP & TTS)
|
||||
// ============================================================================
|
||||
Widget _buildPhoneticsStudioView() {
|
||||
final activeUnit = _vocabUnits[_selectedVocabUnitIndex];
|
||||
final words = activeUnit['words'] as List;
|
||||
final activeWord = words[_selectedWordIndex] as Map<String, dynamic>;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isCompact = constraints.maxWidth < 760;
|
||||
|
||||
if (isCompact) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Unit selector dropdown
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(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: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}),
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
setState(() {
|
||||
_selectedVocabUnitIndex = val;
|
||||
_selectedWordIndex = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Words chips list
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(words.length, (index) {
|
||||
final item = words[index];
|
||||
final isSelected = _selectedWordIndex == index;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(item['word'] as String),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColors.saqelCyan,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
),
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
onSelected: (_) => setState(() => _selectedWordIndex = index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Phonetics Card
|
||||
_buildPhoneticsWorkbenchCard(activeWord),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Left: Units Selector & Words List
|
||||
@@ -1026,29 +1105,42 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
// Right: Phonetics Audio Workbench
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(
|
||||
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(28),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0D1B2A), Color(0xFF1B263B)],
|
||||
colors: [Color(0xFF0B192C), Color(0xFF1E293B)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
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: 12, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -1058,6 +1150,28 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
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,
|
||||
@@ -1073,7 +1187,7 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
activeWord['ipa'] as String,
|
||||
style: const TextStyle(
|
||||
color: AppColors.saqelCyan,
|
||||
fontSize: 18,
|
||||
fontSize: 19,
|
||||
fontFamily: 'Courier',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -1083,7 +1197,7 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
activeWord['ar'] as String,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Audio Play Button
|
||||
ElevatedButton.icon(
|
||||
@@ -1092,7 +1206,7 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
_isPlayingAudio ? 'جاري النطق...' : 'استمع للنطق الصوتي النقي (Audio Pronunciation)',
|
||||
_isPlayingAudio ? 'جاري النطق البريطاني...' : 'استمع للنطق البريطاني الفصيح (British Audio Pronunciation)',
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -1128,6 +1242,7 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.speaker_1, color: AppColors.guardianAmber, size: 18),
|
||||
onPressed: () => _speakWord(activeWord['sentence'] as String),
|
||||
tooltip: 'استمع للجملة',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1140,10 +1255,6 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+90
-52
@@ -431,13 +431,43 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
final activeEx = _textbookExercises[_selectedExerciseIndex];
|
||||
final points = activeEx['points'] as List<Offset>;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isMobile = constraints.maxWidth < 720;
|
||||
|
||||
if (isMobile) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 330,
|
||||
child: _buildCanvasContainer(points),
|
||||
),
|
||||
_buildControlsPanel(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Left: Cartesian Graph Canvas
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: _buildCanvasContainer(points),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _buildControlsPanel(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCanvasContainer(List<Offset> points) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF070C16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -469,8 +499,8 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
|
||||
// Controls Overlay (Zoom)
|
||||
Positioned(
|
||||
top: 14,
|
||||
left: 14,
|
||||
top: 10,
|
||||
left: 10,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -483,12 +513,12 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.zoom_in, color: Colors.white, size: 18),
|
||||
onPressed: () => setState(() => _scale = (_scale + 4).clamp(12.0, 45.0)),
|
||||
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(12.0, 45.0)),
|
||||
onPressed: () => setState(() => _scale = (_scale - 4).clamp(10.0, 50.0)),
|
||||
tooltip: 'تصغير',
|
||||
),
|
||||
IconButton(
|
||||
@@ -503,12 +533,12 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
|
||||
// Curves Legend Badge
|
||||
Positioned(
|
||||
bottom: 14,
|
||||
right: 14,
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(200),
|
||||
color: Colors.black.withAlpha(210),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
@@ -518,41 +548,41 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(width: 12, height: 3, color: AppColors.saqelCyan),
|
||||
const SizedBox(width: 8),
|
||||
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: 11, fontWeight: FontWeight.w600),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Container(width: 12, height: 3, color: AppColors.guardianAmber),
|
||||
const SizedBox(width: 8),
|
||||
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: 11, fontWeight: FontWeight.w600),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: const BoxDecoration(color: Color(0xFFFF2D55), shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
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: 11, fontWeight: FontWeight.w700),
|
||||
style: const TextStyle(color: Color(0xFFFF2D55), fontSize: 10.5, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -563,21 +593,18 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Right: Sliders & Controls Panel
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16, bottom: 16, left: 16),
|
||||
padding: const EdgeInsets.all(18),
|
||||
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: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -590,7 +617,7 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
'غيّر المعاملات وشاهد كيف تتحرك المنحنيات وتتغير نقاط التقاطع فورياً',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 24),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 20),
|
||||
|
||||
// Curve 1 controls
|
||||
Text(
|
||||
@@ -615,7 +642,7 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 8),
|
||||
// Curve 2 controls
|
||||
Text(
|
||||
_curve2Type == 0 ? 'معاملات المستقيم (الميل والمقطع)' : 'معاملات القطع المكافئ (الإزاحة الرأسية)',
|
||||
@@ -670,7 +697,7 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
// Quick Textbook Presets Buttons
|
||||
Builder(
|
||||
builder: (context) {
|
||||
@@ -713,10 +740,6 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1001,18 +1024,20 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (size.width <= 0 || size.height <= 0) return;
|
||||
final cx = size.width / 2;
|
||||
final cy = size.height / 2;
|
||||
final safeScale = scale.clamp(6.0, 60.0);
|
||||
|
||||
// 1. Draw Grid Lines
|
||||
final gridPaint = Paint()
|
||||
..color = const Color(0xFF1B263B)
|
||||
..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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1032,16 +1057,20 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
|
||||
if (curve1Type == 0) {
|
||||
// Circle: x² + y² = r²
|
||||
final rPixel = math.sqrt(radiusSquared) * scale;
|
||||
final safeR2 = radiusSquared > 0 ? radiusSquared : 0.0;
|
||||
final rPixel = math.sqrt(safeR2) * safeScale;
|
||||
if (rPixel.isFinite && rPixel > 0) {
|
||||
canvas.drawCircle(Offset(cx, cy), rPixel, curve1Paint);
|
||||
}
|
||||
} else {
|
||||
// Parabola: y = a1*x² + c1
|
||||
final path = Path();
|
||||
bool started = false;
|
||||
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 py = cy - mathY * scale;
|
||||
final py = cy - mathY * safeScale;
|
||||
if (!py.isFinite) continue;
|
||||
if (!started) {
|
||||
path.moveTo(px, py);
|
||||
started = true;
|
||||
@@ -1049,8 +1078,10 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
path.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
if (started) {
|
||||
canvas.drawPath(path, curve1Paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Draw Curve 2 (Amber)
|
||||
final curve2Paint = Paint()
|
||||
@@ -1060,23 +1091,26 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
|
||||
if (curve2Type == 0) {
|
||||
// Line: y = m*x + b
|
||||
final x1 = (0 - cx) / scale;
|
||||
final x1 = (0 - cx) / safeScale;
|
||||
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 py2 = cy - y2 * scale;
|
||||
final py2 = cy - y2 * safeScale;
|
||||
|
||||
if (py1.isFinite && py2.isFinite) {
|
||||
canvas.drawLine(Offset(0, py1), Offset(size.width, py2), curve2Paint);
|
||||
}
|
||||
} else {
|
||||
// Parabola: y = x² + c2
|
||||
final path = Path();
|
||||
bool started = false;
|
||||
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 py = cy - mathY * scale;
|
||||
final py = cy - mathY * safeScale;
|
||||
if (!py.isFinite) continue;
|
||||
if (!started) {
|
||||
path.moveTo(px, py);
|
||||
started = true;
|
||||
@@ -1084,8 +1118,10 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
path.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
if (started) {
|
||||
canvas.drawPath(path, curve2Paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Draw Intersection Points (Red Glow)
|
||||
final ptGlow = Paint()
|
||||
@@ -1097,8 +1133,10 @@ class _CartesianCanvasPainter extends CustomPainter {
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
for (var pt in intersectionPoints) {
|
||||
final px = cx + pt.dx * scale;
|
||||
final py = cy - pt.dy * scale;
|
||||
if (!pt.dx.isFinite || !pt.dy.isFinite) continue;
|
||||
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), 5.0, ptPaint);
|
||||
|
||||
+1679
-413
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 '../curriculum/subjects_grid_screen.dart';
|
||||
import '../notebook/smart_error_notebook_screen.dart';
|
||||
import '../vocational/vocational_training_screen.dart';
|
||||
|
||||
class UnifiedHomeScreen extends StatefulWidget {
|
||||
final UserModel user;
|
||||
@@ -319,6 +320,71 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
|
||||
),
|
||||
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
|
||||
InkWell(
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user