Update Saqel Platform: 2026-09-13 16:31:17
This commit is contained in:
+1008
-747
File diff suppressed because it is too large
Load Diff
+575
@@ -0,0 +1,575 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE - GRADE 10 DIGITAL SKILLS INTERACTIVE LAB
|
||||
/// ==============================================================================
|
||||
/// مختبر المهارات الرقمية التفاعلي المعتمد للصف العاشر:
|
||||
/// 1. معمل تمثيل البيانات والنظام الثنائي وبكسل الألوان (Binary, ASCII & RGB).
|
||||
/// 2. معمل تحليل البيانات ومعادلات Excel والرسم البياني المرئي.
|
||||
/// 3. محاكي إنترنت الأشياء (IoT) والذكاء الاصطناعي وشجرة القرارات.
|
||||
class DigitalSkillsInteractiveLabView extends StatefulWidget {
|
||||
const DigitalSkillsInteractiveLabView({super.key});
|
||||
|
||||
@override
|
||||
State<DigitalSkillsInteractiveLabView> createState() =>
|
||||
_DigitalSkillsInteractiveLabViewState();
|
||||
}
|
||||
|
||||
class _DigitalSkillsInteractiveLabViewState
|
||||
extends State<DigitalSkillsInteractiveLabView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab 1: Binary & RGB Data Representation
|
||||
// ---------------------------------------------------------------------------
|
||||
final List<bool> _binaryBits = [false, false, false, false, false, true, false, true]; // 5 in 8-bit
|
||||
int _redVal = 30;
|
||||
int _greenVal = 210;
|
||||
int _blueVal = 180;
|
||||
|
||||
int get _decimalValue {
|
||||
int val = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (_binaryBits[i]) {
|
||||
val += (1 << (7 - i));
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
String get _hexValue => _decimalValue.toRadixString(16).toUpperCase().padLeft(2, '0');
|
||||
|
||||
String get _asciiChar {
|
||||
final d = _decimalValue;
|
||||
if (d >= 32 && d <= 126) {
|
||||
return String.fromCharCode(d);
|
||||
}
|
||||
return 'غير قابل للطباعة (Control Code)';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab 2: Excel & Data Analytics
|
||||
// ---------------------------------------------------------------------------
|
||||
final List<Map<String, dynamic>> _dataRows = [
|
||||
{'item': 'عمان', 'val': 85.0},
|
||||
{'item': 'إربد', 'val': 72.0},
|
||||
{'item': 'الزرقاء', 'val': 68.0},
|
||||
{'item': 'العقبة', 'val': 94.0},
|
||||
{'item': 'البلقاء', 'val': 61.0},
|
||||
];
|
||||
|
||||
double get _calcSum => _dataRows.fold(0.0, (acc, r) => acc + (r['val'] as double));
|
||||
double get _calcAvg => _dataRows.isNotEmpty ? _calcSum / _dataRows.length : 0.0;
|
||||
double get _calcMax => _dataRows.fold(0.0, (max, r) => (r['val'] as double) > max ? (r['val'] as double) : max);
|
||||
double get _calcMin => _dataRows.fold(999.0, (min, r) => (r['val'] as double) < min ? (r['val'] as double) : min);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab 3: IoT Sensors & AI Decision Tree
|
||||
// ---------------------------------------------------------------------------
|
||||
double _temperature = 28.0;
|
||||
double _humidity = 45.0;
|
||||
bool _motionDetected = false;
|
||||
|
||||
String get _aiDecision {
|
||||
if (_temperature > 32.0 && _humidity > 60.0) {
|
||||
return 'تشغيل نظام التكييف الذكي وخفض الرطوبة (تحذير إجهاد حراري)';
|
||||
} else if (_temperature < 16.0) {
|
||||
return 'تشغيل نظام التدفئة المركزية والتهوية الآمنة';
|
||||
} else if (_motionDetected) {
|
||||
return 'تفعيل الإضاءة الذكية وتسجيل نشاط الكاميرا';
|
||||
}
|
||||
return 'البيئة مستقرة ومثالية • الاستهلاك الاقتصادي للطاقة مفعل';
|
||||
}
|
||||
|
||||
Color get _aiStatusColor {
|
||||
if (_temperature > 32.0 || _temperature < 16.0) {
|
||||
return const Color(0xFFF59E0B);
|
||||
}
|
||||
if (_motionDetected) {
|
||||
return const Color(0xFF38BDF8);
|
||||
}
|
||||
return const Color(0xFF10B981);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: AppColors.saqelCyan,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: AppColors.textSecondaryDark,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w800, fontSize: 11.5),
|
||||
tabs: const [
|
||||
Tab(text: 'تمثيل البيانات والثنائي 0101'),
|
||||
Tab(text: 'تحليل البيانات وExcel 📊'),
|
||||
Tab(text: 'إنترنت الأشياء والذكاء 🤖'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildBinaryLab(),
|
||||
_buildExcelLab(),
|
||||
_buildIotLab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBinaryLab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 8-bit Binary Switcher Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: AppColors.saqelCyan.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.chevron_left_slash_chevron_right, color: AppColors.saqelCyan, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'محاكي بايت البيانات الثنائي (8-Bit Binary Byte):',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'انقر على البت لتشغيله (1) أو إطفائه (0) وشاهد تحويل القيمة فورياً:',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 8 Switches
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(8, (i) {
|
||||
final weight = 1 << (7 - i);
|
||||
final isSet = _binaryBits[i];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_binaryBits[i] = !_binaryBits[i];
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'$weight',
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 10, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 34,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: isSet ? AppColors.saqelCyan.withValues(alpha: 0.25) : Colors.black26,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSet ? AppColors.saqelCyan : Colors.white24,
|
||||
width: isSet ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
isSet ? '1' : '0',
|
||||
style: TextStyle(
|
||||
color: isSet ? AppColors.saqelCyan : Colors.white38,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Computed Output Badges
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildResultBadge('العشري (Decimal)', '$_decimalValue'),
|
||||
_buildResultBadge('الست عشري (Hex)', '0x$_hexValue'),
|
||||
_buildResultBadge('حرف ASCII', _asciiChar),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// RGB Pixel Inspector
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.paintbrush_fill, color: Color(0xFFFF375F), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'مفتش البكسل اللوني وتمثيل الصور (24-bit RGB):',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
// Color Preview Box
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromARGB(255, _redVal, _greenVal, _blueVal),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white24, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(255, _redVal, _greenVal, _blueVal).withValues(alpha: 0.5),
|
||||
blurRadius: 14,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'RGB($_redVal, $_greenVal, $_blueVal)',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'حجم البكسل: 3 بايت = 24 بت في الذاكرة (16.7 مليون لون ممكن)',
|
||||
style: TextStyle(color: Colors.white.withValues(alpha: 0.7), fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildColorSlider('أحمر (Red R)', _redVal, const Color(0xFFFF453A), (v) => setState(() => _redVal = v.round())),
|
||||
_buildColorSlider('أخضر (Green G)', _greenVal, const Color(0xFF30D158), (v) => setState(() => _greenVal = v.round())),
|
||||
_buildColorSlider('أزرق (Blue B)', _blueVal, const Color(0xFF0A84FF), (v) => setState(() => _blueVal = v.round())),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExcelLab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Formulas Summary Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF10B981).withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildStatColumn('دالة المجموع', '=SUM(B2:B6)', _calcSum.toStringAsFixed(0), const Color(0xFF10B981)),
|
||||
_buildStatColumn('دالة المتوسط', '=AVERAGE(B2:B6)', _calcAvg.toStringAsFixed(1), const Color(0xFF38BDF8)),
|
||||
_buildStatColumn('أعلى قيمة', '=MAX(B2:B6)', _calcMax.toStringAsFixed(0), const Color(0xFFF59E0B)),
|
||||
_buildStatColumn('أدنى قيمة', '=MIN(B2:B6)', _calcMin.toStringAsFixed(0), const Color(0xFFEC4899)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Interactive Data Table & Bar Chart
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.table, color: Color(0xFF10B981), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'جدول البيانات والتمثيل المرئي (Data Visualization):',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
..._dataRows.map((row) {
|
||||
final val = row['val'] as double;
|
||||
final ratio = (_calcMax > 0) ? (val / _calcMax) : 0.0;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
row['item'],
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12.5),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: LinearProgressIndicator(
|
||||
value: ratio,
|
||||
minHeight: 14,
|
||||
backgroundColor: Colors.white10,
|
||||
valueColor: const AlwaysStoppedAnimation(Color(0xFF10B981)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
val.toStringAsFixed(0),
|
||||
style: const TextStyle(color: Color(0xFF10B981), fontWeight: FontWeight.w900, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIotLab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// IoT Sensor Sliders Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.radiowaves_right, color: AppColors.saqelCyan, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'محاكي مجسات إنترنت الأشياء (IoT Smart Sensors):',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Temperature Slider
|
||||
Text(
|
||||
'حساس درجة الحرارة (DHT22): ${_temperature.toStringAsFixed(1)} °C',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12.5),
|
||||
),
|
||||
Slider(
|
||||
value: _temperature,
|
||||
min: 10.0,
|
||||
max: 45.0,
|
||||
divisions: 35,
|
||||
activeColor: const Color(0xFFFF9F0A),
|
||||
onChanged: (v) => setState(() => _temperature = v),
|
||||
),
|
||||
|
||||
// Humidity Slider
|
||||
Text(
|
||||
'حساس الرطوبة النسبية: ${_humidity.toStringAsFixed(0)} %',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12.5),
|
||||
),
|
||||
Slider(
|
||||
value: _humidity,
|
||||
min: 20.0,
|
||||
max: 95.0,
|
||||
divisions: 75,
|
||||
activeColor: const Color(0xFF38BDF8),
|
||||
onChanged: (v) => setState(() => _humidity = v),
|
||||
),
|
||||
|
||||
// Motion Detection Switch
|
||||
Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.person_crop_circle_badge_exclam, color: Colors.white70, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
const Text('مستشعر الحركة بالأشعة (PIR):', style: TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
CupertinoSwitch(
|
||||
value: _motionDetected,
|
||||
activeTrackColor: AppColors.saqelCyan,
|
||||
onChanged: (v) => setState(() => _motionDetected = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// AI Decision Output Box
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _aiStatusColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _aiStatusColor),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.sparkles, color: _aiStatusColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'قرار الذكاء الاصطناعي (AI Decision Engine):',
|
||||
style: TextStyle(color: _aiStatusColor, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
_aiDecision,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13.5, height: 1.45, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultBadge(String label, String val) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11)),
|
||||
const SizedBox(height: 4),
|
||||
Text(val, style: const TextStyle(color: AppColors.saqelCyan, fontSize: 16, fontWeight: FontWeight.w900)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorSlider(String label, int value, Color col, ValueChanged<double> onChanged) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: Text(label, style: TextStyle(color: col, fontSize: 11.5, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: value.toDouble(),
|
||||
min: 0,
|
||||
max: 255,
|
||||
activeColor: col,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 32,
|
||||
child: Text('$value', style: const TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatColumn(String label, String formula, String val, Color col) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 10.5, fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 2),
|
||||
Text(formula, style: TextStyle(color: col, fontSize: 9.5, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
Text(val, style: TextStyle(color: col, fontSize: 16, fontWeight: FontWeight.w900)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+929
-900
File diff suppressed because it is too large
Load Diff
+1200
-338
File diff suppressed because it is too large
Load Diff
+424
-775
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/app_logger.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
@@ -30,6 +31,7 @@ import 'islamic_interactive_lab_view.dart';
|
||||
import 'history_interactive_timeline_view.dart';
|
||||
import 'math_interactive_lab_view.dart';
|
||||
import 'english_interactive_lab_view.dart';
|
||||
import 'digital_skills_interactive_lab_view.dart';
|
||||
import '../virtual_labs/labs_gallery_screen.dart';
|
||||
import '../virtual_labs/labs_registry.dart';
|
||||
import '../virtual_labs/subject_virtual_labs_view.dart';
|
||||
@@ -54,6 +56,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
late Future<List<ExamModel>> _examsFuture;
|
||||
String _selectedSemester = 'semester_1';
|
||||
|
||||
bool get _isMathSubject =>
|
||||
widget.subject.id.contains('math') ||
|
||||
@@ -101,6 +104,12 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
(widget.subject.title.contains('تاريخ') && !widget.subject.title.contains('جغرافيا')) ||
|
||||
(widget.subject.title.contains('أردن') && !widget.subject.title.contains('جغرافيا'));
|
||||
|
||||
bool get _isDigitalSkillsSubject =>
|
||||
widget.subject.id.contains('digital') ||
|
||||
widget.subject.id.contains('computer') ||
|
||||
widget.subject.title.contains('رقمي') ||
|
||||
widget.subject.title.contains('حاسوب');
|
||||
|
||||
bool get _hasLab =>
|
||||
Grade10LabsRegistry.bySubjectNormalized(widget.subject.title).isNotEmpty ||
|
||||
_isMathSubject ||
|
||||
@@ -111,7 +120,8 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
_isPhysicsSubject ||
|
||||
_isArabicSubject ||
|
||||
_isIslamicSubject ||
|
||||
_isHistorySubject;
|
||||
_isHistorySubject ||
|
||||
_isDigitalSkillsSubject;
|
||||
|
||||
Tab get _dynamicLabTab {
|
||||
final norm = Grade10LabsRegistry.normalizeSubject(widget.subject.title);
|
||||
@@ -133,12 +143,12 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
} else if (_isIslamicSubject || norm == 'التربية الإسلامية') {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.money_dollar_circle_fill, size: 18),
|
||||
text: 'حاسبة المواريث والتجويد ⚖️',
|
||||
text: 'معمل المعاملات والتجويد ⚖️',
|
||||
);
|
||||
} else if (_isHistorySubject || norm == 'تاريخ الأردن' || norm == 'التاريخ') {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.time_solid, size: 18),
|
||||
text: 'الخط الزمني الشامل والتاريخ 🏛️',
|
||||
text: 'مختبر التاريخ والخط الزمني 🏛️',
|
||||
);
|
||||
} else if (_isChemistrySubject || norm == 'الكيمياء') {
|
||||
return const Tab(
|
||||
@@ -160,10 +170,10 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
icon: Icon(CupertinoIcons.chart_bar_square_fill, size: 18),
|
||||
text: 'مختبر الثقافة المالية والجدوى 📊',
|
||||
);
|
||||
} else if (norm == 'المهارات الرقمية') {
|
||||
} else if (_isDigitalSkillsSubject || norm == 'المهارات الرقمية') {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.desktopcomputer, size: 18),
|
||||
text: 'مختبر الخوارزميات والبرمجة 💻',
|
||||
text: 'معمل البيانات والذكاء الاصطناعي 💻',
|
||||
);
|
||||
} else if (norm == 'الجغرافيا') {
|
||||
return const Tab(
|
||||
@@ -204,10 +214,13 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
specializedTitle = 'شجرة الإعراب 📜';
|
||||
} else if (_isIslamicSubject || norm == 'التربية الإسلامية') {
|
||||
specializedTool = const IslamicInteractiveLabView();
|
||||
specializedTitle = 'حاسبة المواريث ⚖️';
|
||||
specializedTitle = 'معمل المعاملات والتجويد ⚖️';
|
||||
} else if (_isHistorySubject || norm == 'تاريخ الأردن' || norm == 'التاريخ') {
|
||||
specializedTool = const HistoryInteractiveTimelineView();
|
||||
specializedTitle = 'الخط الزمني الشامل 🏛️';
|
||||
specializedTitle = 'الخط الزمني الشامل والتاريخ 🏛️';
|
||||
} else if (_isDigitalSkillsSubject || norm == 'المهارات الرقمية') {
|
||||
specializedTool = const DigitalSkillsInteractiveLabView();
|
||||
specializedTitle = 'معمل البيانات والذكاء 💻';
|
||||
} else if (_isChemistrySubject || norm == 'الكيمياء') {
|
||||
specializedTool = const ChemistryInteractiveLabView();
|
||||
specializedTitle = 'طيف ذرة بور ⚗️';
|
||||
@@ -227,6 +240,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
primaryColor: widget.subject.primaryColor,
|
||||
specializedToolWidget: specializedTool,
|
||||
specializedToolTitle: specializedTitle,
|
||||
selectedSemester: _selectedSemester,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,9 +324,115 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/// Semester Switcher Component for Multi-Semester Subjects
|
||||
Widget _buildSemesterSelector() {
|
||||
final s1Count = widget.subject.semester1Units.length;
|
||||
final s2Count = widget.subject.semester2Units.length;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(18, 14, 18, 6),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSemesterSegment(
|
||||
key: 'semester_1',
|
||||
title: 'الفصل الدراسي الأول',
|
||||
count: s1Count,
|
||||
isSelected: _selectedSemester == 'semester_1',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: _buildSemesterSegment(
|
||||
key: 'semester_2',
|
||||
title: 'الفصل الدراسي الثاني',
|
||||
count: s2Count,
|
||||
isSelected: _selectedSemester == 'semester_2',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSemesterSegment({
|
||||
required String key,
|
||||
required String title,
|
||||
required int count,
|
||||
required bool isSelected,
|
||||
}) {
|
||||
final color = widget.subject.primaryColor;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (_selectedSemester != key) {
|
||||
setState(() {
|
||||
_selectedSemester = key;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color.withValues(alpha: 0.18) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSelected ? color.withValues(alpha: 0.6) : Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? CupertinoIcons.checkmark_seal_fill : CupertinoIcons.calendar,
|
||||
size: 14,
|
||||
color: isSelected ? color : AppColors.textSecondaryDark,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : AppColors.textSecondaryDark,
|
||||
fontSize: 12.5,
|
||||
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (count > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color.withValues(alpha: 0.3) : AppColors.darkCardBorder,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'$count وحدات',
|
||||
style: TextStyle(
|
||||
color: isSelected ? color : AppColors.textSecondaryDark,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Tab 1: Interactive Video Lessons by Unit
|
||||
Widget _buildLessonsTab(BuildContext context) {
|
||||
final units = widget.subject.units;
|
||||
final hasMultipleSemesters = widget.subject.semester1Units.isNotEmpty && widget.subject.semester2Units.isNotEmpty;
|
||||
final units = hasMultipleSemesters
|
||||
? widget.subject.unitsForSemester(_selectedSemester)
|
||||
: widget.subject.units;
|
||||
|
||||
if (units.isEmpty) {
|
||||
return Center(
|
||||
@@ -330,11 +450,15 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||
itemCount: units.length,
|
||||
itemBuilder: (context, uIdx) {
|
||||
final unit = units[uIdx];
|
||||
return Column(
|
||||
children: [
|
||||
if (hasMultipleSemesters) _buildSemesterSelector(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
itemCount: units.length,
|
||||
itemBuilder: (context, uIdx) {
|
||||
final unit = units[uIdx];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
@@ -380,6 +504,9 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
lessonTitle: lesson.title,
|
||||
lessonId: lesson.id,
|
||||
curriculumLessonId: lesson.curriculumLessonId,
|
||||
filePath: lesson.markdownFilePath,
|
||||
unitKey: unit.id,
|
||||
semesterKey: _selectedSemester,
|
||||
);
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
@@ -455,6 +582,13 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
size: 16,
|
||||
),
|
||||
onTap: () {
|
||||
AppLogger.event('LessonTileTapped', details: {
|
||||
'subject': widget.subject.title,
|
||||
'lessonTitle': lesson.title,
|
||||
'lessonId': lesson.id,
|
||||
'hasVideo': hasVid,
|
||||
'curriculumLessonId': lesson.curriculumLessonId,
|
||||
}, tag: 'SUBJECT_HUB');
|
||||
_showLessonVideoSelector(context, lesson);
|
||||
},
|
||||
),
|
||||
@@ -463,7 +597,15 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => Grade10LabsRegistry.openLab(context, lab),
|
||||
onTap: () {
|
||||
AppLogger.event('OpenLessonLab', details: {
|
||||
'subject': widget.subject.title,
|
||||
'lesson': lesson.title,
|
||||
'lab': lab.lessonAr,
|
||||
'curriculumId': lab.identity.curriculumLessonId,
|
||||
}, tag: 'SUBJECT_HUB');
|
||||
Grade10LabsRegistry.openLab(context, lab);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
@@ -476,14 +618,42 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
const Icon(CupertinoIcons.lab_flask_solid, color: AppColors.saqelCyan, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'المختبر الافتراضي: ${lab.lessonAr}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'المختبر: ${lab.lessonAr}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: lab.identity.isPublished
|
||||
? AppColors.teacherEmerald.withAlpha(35)
|
||||
: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
lab.identity.isPublished
|
||||
? 'معتمد 🟢'
|
||||
: (lab.identity.isBound ? 'جاهز للاستخدام 🟢' : 'معاينة 🔵'),
|
||||
style: TextStyle(
|
||||
color: lab.identity.isPublished
|
||||
? AppColors.teacherEmerald
|
||||
: AppColors.saqelCyan,
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -513,62 +683,32 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_hasLab)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
if (_tabController.length > 1) {
|
||||
_tabController.animateTo(1);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.subject.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: widget.subject.primaryColor.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.lab_flask_solid, color: widget.subject.primaryColor, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'مختبر ${widget.subject.title} التفاعلي المعتمد',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(6),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.white.withAlpha(12)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.lab_flask, color: Colors.white38, size: 13),
|
||||
SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'المختبر الافتراضي: قيد الإعداد الأكاديمي ⚪',
|
||||
style: TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.subject.primaryColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'انتقل للمختبر',
|
||||
style: TextStyle(
|
||||
color: widget.subject.primaryColor,
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Icon(CupertinoIcons.arrow_left, color: widget.subject.primaryColor, size: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -634,6 +774,9 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1098,6 +1241,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
lessonTitle: lesson.title,
|
||||
lessonId: lesson.id,
|
||||
curriculumLessonId: lesson.curriculumLessonId,
|
||||
filePath: lesson.markdownFilePath,
|
||||
);
|
||||
|
||||
showModalBottomSheet(
|
||||
|
||||
Reference in New Issue
Block a user