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

This commit is contained in:
Hamza-Ayed
2026-09-05 22:17:15 +03:00
parent 68265a5bc6
commit 0e5be7adbb
6 changed files with 3322 additions and 885 deletions
@@ -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,27 +929,29 @@ 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 Row(
children: [
// Left: Units Selector & Words List
Container(
width: 280,
decoration: const BoxDecoration(
color: AppColors.darkSurface,
border: Border(left: BorderSide(color: AppColors.darkCardBorder)),
),
child: Column(
return LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxWidth < 760;
if (isCompact) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Unit Dropdown
// Unit selector dropdown
Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)),
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>(
@@ -961,7 +963,7 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
value: i,
child: Text(
_vocabUnits[i]['unit'] as String,
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold),
),
);
}),
@@ -976,171 +978,280 @@ class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
),
),
),
const SizedBox(height: 12),
// Words List
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: words.length,
itemBuilder: (context, index) {
// Words chips list
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(words.length, (index) {
final item = words[index];
final isSelected = _selectedWordIndex == index;
return Container(
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
color: isSelected ? AppColors.saqelCyan.withAlpha(25) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
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,
),
),
child: ListTile(
dense: true,
title: Text(
item['word'] as String,
style: TextStyle(
color: isSelected ? AppColors.saqelCyan : Colors.white,
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
),
),
subtitle: Text(
item['ar'] as String,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
trailing: IconButton(
icon: const Icon(CupertinoIcons.volume_up, size: 16, color: AppColors.saqelCyan),
onPressed: () => _speakWord(item['word'] as String),
),
onTap: () => setState(() => _selectedWordIndex = index),
backgroundColor: AppColors.darkSurface,
onSelected: (_) => setState(() => _selectedWordIndex = index),
),
);
},
}),
),
),
const SizedBox(height: 16),
// Phonetics Card
_buildPhoneticsWorkbenchCard(activeWord),
],
);
}
return Row(
children: [
// Left: Units Selector & Words List
Container(
width: 280,
decoration: const BoxDecoration(
color: AppColors.darkSurface,
border: Border(left: BorderSide(color: AppColors.darkCardBorder)),
),
child: Column(
children: [
// Unit Dropdown
Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _selectedVocabUnitIndex,
isExpanded: true,
dropdownColor: AppColors.darkSurface,
items: List.generate(_vocabUnits.length, (i) {
return DropdownMenuItem(
value: i,
child: Text(
_vocabUnits[i]['unit'] as String,
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}),
onChanged: (val) {
if (val != null) {
setState(() {
_selectedVocabUnitIndex = val;
_selectedWordIndex = 0;
});
}
},
),
),
),
// Words List
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: words.length,
itemBuilder: (context, index) {
final item = words[index];
final isSelected = _selectedWordIndex == index;
return Container(
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
color: isSelected ? AppColors.saqelCyan.withAlpha(25) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
),
),
child: ListTile(
dense: true,
title: Text(
item['word'] as String,
style: TextStyle(
color: isSelected ? AppColors.saqelCyan : Colors.white,
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
),
),
subtitle: Text(
item['ar'] as String,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
trailing: IconButton(
icon: const Icon(CupertinoIcons.volume_up, size: 16, color: AppColors.saqelCyan),
onPressed: () => _speakWord(item['word'] as String),
),
onTap: () => setState(() => _selectedWordIndex = index),
),
);
},
),
),
],
),
),
// Right: Phonetics Audio Workbench
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: _buildPhoneticsWorkbenchCard(activeWord),
),
),
],
);
},
);
}
Widget _buildPhoneticsWorkbenchCard(Map<String, dynamic> activeWord) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Big Pronunciation Header Card
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0B192C), Color(0xFF1E293B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.saqelCyan.withAlpha(90)),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 20, offset: Offset(0, 10)),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(30),
borderRadius: BorderRadius.circular(16),
),
child: Text(
activeWord['pos'] as String,
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF1E3A8A).withAlpha(160),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF60A5FA).withAlpha(120)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('🇬🇧', style: TextStyle(fontSize: 12)),
SizedBox(width: 4),
Text(
'British RP',
style: TextStyle(color: Color(0xFF93C5FD), fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
),
],
),
const SizedBox(height: 14),
Text(
activeWord['word'] as String,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
const SizedBox(height: 6),
Text(
activeWord['ipa'] as String,
style: const TextStyle(
color: AppColors.saqelCyan,
fontSize: 19,
fontFamily: 'Courier',
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 10),
Text(
activeWord['ar'] as String,
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 18),
// Audio Play Button
ElevatedButton.icon(
icon: Icon(
_isPlayingAudio ? CupertinoIcons.waveform : CupertinoIcons.volume_up,
size: 20,
),
label: Text(
_isPlayingAudio ? 'جاري النطق البريطاني...' : 'استمع للنطق البريطاني الفصيح (British Audio Pronunciation)',
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
onPressed: () => _speakWord(activeWord['word'] as String),
),
],
),
),
// Right: Phonetics Audio Workbench
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Big Pronunciation Header Card
Container(
padding: const EdgeInsets.all(28),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0D1B2A), Color(0xFF1B263B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 20, offset: Offset(0, 10)),
],
const SizedBox(height: 20),
// Detailed Definition & Collocations
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('التعريف المعتمد (Official Definition):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Text(activeWord['def'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5)),
const Divider(color: AppColors.darkCardBorder, height: 24),
const Text('جملة المنهاج الوزاري (Textbook Context):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Text(activeWord['sentence'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5, fontStyle: FontStyle.italic)),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(30),
borderRadius: BorderRadius.circular(16),
),
child: Text(
activeWord['pos'] as String,
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 14),
Text(
activeWord['word'] as String,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
const SizedBox(height: 6),
Text(
activeWord['ipa'] as String,
style: const TextStyle(
color: AppColors.saqelCyan,
fontSize: 18,
fontFamily: 'Courier',
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 10),
Text(
activeWord['ar'] as String,
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// Audio Play Button
ElevatedButton.icon(
icon: Icon(
_isPlayingAudio ? CupertinoIcons.waveform : CupertinoIcons.volume_up,
size: 20,
),
label: Text(
_isPlayingAudio ? 'جاري النطق...' : 'استمع للنطق الصوتي النقي (Audio Pronunciation)',
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
onPressed: () => _speakWord(activeWord['word'] as String),
),
],
IconButton(
icon: const Icon(CupertinoIcons.speaker_1, color: AppColors.guardianAmber, size: 18),
onPressed: () => _speakWord(activeWord['sentence'] as String),
tooltip: 'استمع للجملة',
),
),
],
),
const Divider(color: AppColors.darkCardBorder, height: 24),
const SizedBox(height: 20),
// Detailed Definition & Collocations
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('التعريف المعتمد (Official Definition):', style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Text(activeWord['def'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5)),
const Divider(color: AppColors.darkCardBorder, height: 24),
const Text('جملة المنهاج الوزاري (Textbook Context):', style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Text(activeWord['sentence'] as String, style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5, fontStyle: FontStyle.italic)),
),
IconButton(
icon: const Icon(CupertinoIcons.speaker_1, color: AppColors.guardianAmber, size: 18),
onPressed: () => _speakWord(activeWord['sentence'] as String),
),
],
),
const Divider(color: AppColors.darkCardBorder, height: 24),
const Text('المتلازمات اللفظية (Collocations):', style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Text(activeWord['collocations'] as String, style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
),
],
),
const Text('المتلازمات اللفظية (Collocations):', style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 6),
Text(activeWord['collocations'] as String, style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
),
],
@@ -431,292 +431,315 @@ class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
final activeEx = _textbookExercises[_selectedExerciseIndex];
final points = activeEx['points'] as List<Offset>;
return Row(
children: [
// Left: Cartesian Graph Canvas
Expanded(
flex: 6,
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF070C16),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 8)),
return LayoutBuilder(
builder: (context, constraints) {
final isMobile = constraints.maxWidth < 720;
if (isMobile) {
return SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: 330,
child: _buildCanvasContainer(points),
),
_buildControlsPanel(),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: _CartesianCanvasPainter(
curve1Type: _curve1Type,
radiusSquared: _radiusSquared,
a1: _a1,
c1: _c1,
curve2Type: _curve2Type,
m: _m,
b: _b,
c2: _c2,
scale: _scale,
intersectionPoints: points,
),
),
),
);
}
// Controls Overlay (Zoom)
Positioned(
top: 14,
left: 14,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(180),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white24),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(CupertinoIcons.zoom_in, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale + 4).clamp(12.0, 45.0)),
tooltip: 'تكبير',
),
IconButton(
icon: const Icon(CupertinoIcons.zoom_out, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale - 4).clamp(12.0, 45.0)),
tooltip: 'تصغير',
),
IconButton(
icon: const Icon(CupertinoIcons.refresh, color: AppColors.saqelCyan, size: 18),
onPressed: () => setState(() => _scale = 22.0),
tooltip: 'إعادة ضبط المحاور',
),
],
),
),
),
return Row(
children: [
Expanded(
flex: 6,
child: _buildCanvasContainer(points),
),
Expanded(
flex: 4,
child: _buildControlsPanel(),
),
],
);
},
);
}
// Curves Legend Badge
Positioned(
bottom: 14,
right: 14,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withAlpha(200),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(width: 12, height: 3, color: AppColors.saqelCyan),
const SizedBox(width: 8),
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),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Container(width: 12, height: 3, color: AppColors.guardianAmber),
const SizedBox(width: 8),
Text(
_curve2Type == 0
? 'المستقيم: y = ${_m.toStringAsFixed(1)}x + ${_b.toStringAsFixed(1)}'
: 'القطع المكافئ 2: y = x² + ${_c2.toStringAsFixed(1)}',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(color: Color(0xFFFF2D55), shape: BoxShape.circle),
),
const SizedBox(width: 8),
Text(
'نقاط التقاطع: ${points.isEmpty ? "لا يوجد حل حقيقي (∅)" : points.map((p) => "(${p.dx.toStringAsFixed(1)}, ${p.dy.toStringAsFixed(1)})").join(" , ")}',
style: const TextStyle(color: Color(0xFFFF2D55), fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
],
),
),
),
],
Widget _buildCanvasContainer(List<Offset> points) {
return Container(
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF070C16),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 16, offset: Offset(0, 8)),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: _CartesianCanvasPainter(
curve1Type: _curve1Type,
radiusSquared: _radiusSquared,
a1: _a1,
c1: _c1,
curve2Type: _curve2Type,
m: _m,
b: _b,
c2: _c2,
scale: _scale,
intersectionPoints: points,
),
),
),
),
),
// Right: Sliders & Controls Panel
Expanded(
flex: 4,
child: Container(
margin: const EdgeInsets.only(top: 16, bottom: 16, left: 16),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
// Controls Overlay (Zoom)
Positioned(
top: 10,
left: 10,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(180),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white24),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(CupertinoIcons.zoom_in, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale + 4).clamp(10.0, 50.0)),
tooltip: 'تكبير',
),
IconButton(
icon: const Icon(CupertinoIcons.zoom_out, color: Colors.white, size: 18),
onPressed: () => setState(() => _scale = (_scale - 4).clamp(10.0, 50.0)),
tooltip: 'تصغير',
),
IconButton(
icon: const Icon(CupertinoIcons.refresh, color: AppColors.saqelCyan, size: 18),
onPressed: () => setState(() => _scale = 22.0),
tooltip: 'إعادة ضبط المحاور',
),
],
),
),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
// Curves Legend Badge
Positioned(
bottom: 10,
right: 10,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withAlpha(210),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(width: 10, height: 3, color: AppColors.saqelCyan),
const SizedBox(width: 6),
Text(
_curve1Type == 0
? 'الدائرة: x² + y² = ${_radiusSquared.toStringAsFixed(1)}'
: 'القطع المكافئ 1: y = ${_a1.toStringAsFixed(1)}x² + ${_c1.toStringAsFixed(1)}',
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 3),
Row(
children: [
Container(width: 10, height: 3, color: AppColors.guardianAmber),
const SizedBox(width: 6),
Text(
_curve2Type == 0
? 'المستقيم: y = ${_m.toStringAsFixed(1)}x + ${_b.toStringAsFixed(1)}'
: 'القطع المكافئ 2: y = x² + ${_c2.toStringAsFixed(1)}',
style: const TextStyle(color: Colors.white, fontSize: 10.5, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 3),
Row(
children: [
Container(
width: 7,
height: 7,
decoration: const BoxDecoration(color: Color(0xFFFF2D55), shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
'نقاط التقاطع: ${points.isEmpty ? "لا يوجد حل حقيقي (∅)" : points.map((p) => "(${p.dx.toStringAsFixed(1)}, ${p.dy.toStringAsFixed(1)})").join(" , ")}',
style: const TextStyle(color: Color(0xFFFF2D55), fontSize: 10.5, fontWeight: FontWeight.w700),
),
],
),
],
),
),
),
],
),
),
);
}
Widget _buildControlsPanel() {
return Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'لوحة المعاملات الحية 🎛️',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
),
const SizedBox(height: 4),
const Text(
'غيّر المعاملات وشاهد كيف تتحرك المنحنيات وتتغير نقاط التقاطع فورياً',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
const Divider(color: AppColors.darkCardBorder, height: 20),
// Curve 1 controls
Text(
_curve1Type == 0 ? 'معاملات الدائرة (r²)' : 'معاملات المنحنى الأول',
style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 12),
),
if (_curve1Type == 0) ...[
Row(
children: [
const Text('نصف القطر التربيعي (r²):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_radiusSquared.toStringAsFixed(1), style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _radiusSquared,
min: 1.0,
max: 25.0,
divisions: 24,
activeColor: AppColors.saqelCyan,
onChanged: (val) => setState(() => _radiusSquared = val),
),
],
const SizedBox(height: 8),
// Curve 2 controls
Text(
_curve2Type == 0 ? 'معاملات المستقيم (الميل والمقطع)' : 'معاملات القطع المكافئ (الإزاحة الرأسية)',
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 12),
),
if (_curve2Type == 0) ...[
Row(
children: [
const Text('الميل (m):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_m.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _m,
min: -5.0,
max: 5.0,
divisions: 20,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _m = val),
),
Row(
children: [
const Text('المقطع الصادي (b):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_b.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _b,
min: -10.0,
max: 10.0,
divisions: 40,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _b = val),
),
] else ...[
Row(
children: [
const Text('الإزاحة الرأسية (c):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_c2.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _c2,
min: -12.0,
max: 6.0,
divisions: 36,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _c2 = val),
),
],
const SizedBox(height: 12),
// Quick Textbook Presets Buttons
Builder(
builder: (context) {
final activeLessons = _curriculumUnits[_selectedUnitIndex]['lessons'] as List<Map<String, dynamic>>;
final activeLesson = activeLessons[_selectedLessonIndex];
final lessonExercises = activeLesson['exercises'] as List<int>;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'لوحة المعاملات الحية 🎛️',
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
),
const SizedBox(height: 4),
const Text(
'غيّر المعاملات وشاهد كيف تتحرك المنحنيات وتتغير نقاط التقاطع فورياً',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
const Divider(color: AppColors.darkCardBorder, height: 24),
// Curve 1 controls
Text(
_curve1Type == 0 ? 'معاملات الدائرة (r²)' : 'معاملات المنحنى الأول',
style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 12),
'تدريبات (${activeLesson['title']}):',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12),
),
if (_curve1Type == 0) ...[
Row(
children: [
const Text('نصف القطر التربيعي (r²):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_radiusSquared.toStringAsFixed(1), style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _radiusSquared,
min: 1.0,
max: 25.0,
divisions: 24,
activeColor: AppColors.saqelCyan,
onChanged: (val) => setState(() => _radiusSquared = val),
),
],
const SizedBox(height: 12),
// Curve 2 controls
Text(
_curve2Type == 0 ? 'معاملات المستقيم (الميل والمقطع)' : 'معاملات القطع المكافئ (الإزاحة الرأسية)',
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 12),
),
if (_curve2Type == 0) ...[
Row(
children: [
const Text('الميل (m):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_m.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _m,
min: -5.0,
max: 5.0,
divisions: 20,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _m = val),
),
Row(
children: [
const Text('المقطع الصادي (b):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_b.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _b,
min: -10.0,
max: 10.0,
divisions: 40,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _b = val),
),
] else ...[
Row(
children: [
const Text('الإزاحة الرأسية (c):', style: TextStyle(color: Colors.white70, fontSize: 11)),
const Spacer(),
Text(_c2.toStringAsFixed(1), style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold)),
],
),
Slider(
value: _c2,
min: -12.0,
max: 6.0,
divisions: 36,
activeColor: AppColors.guardianAmber,
onChanged: (val) => setState(() => _c2 = val),
),
],
const SizedBox(height: 16),
// Quick Textbook Presets Buttons
Builder(
builder: (context) {
final activeLessons = _curriculumUnits[_selectedUnitIndex]['lessons'] as List<Map<String, dynamic>>;
final activeLesson = activeLessons[_selectedLessonIndex];
final lessonExercises = activeLesson['exercises'] as List<int>;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تدريبات (${activeLesson['title']}):',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: lessonExercises.map((i) {
final isSelected = _selectedExerciseIndex == i;
return ChoiceChip(
label: Text(
i == 0 ? 'نشاط ص16' : 'تدريب $i',
style: TextStyle(
color: isSelected ? Colors.black : Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: lessonExercises.map((i) {
final isSelected = _selectedExerciseIndex == i;
return ChoiceChip(
label: Text(
i == 0 ? 'نشاط ص16' : 'تدريب $i',
style: TextStyle(
color: isSelected ? Colors.black : Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
selected: isSelected,
selectedColor: AppColors.saqelCyan,
backgroundColor: AppColors.darkCard,
onSelected: (_) => _applyTextbookPreset(i),
);
}).toList(),
),
],
),
selected: isSelected,
selectedColor: AppColors.saqelCyan,
backgroundColor: AppColors.darkCard,
onSelected: (_) => _applyTextbookPreset(i),
);
},
}).toList(),
),
],
),
),
);
},
),
),
],
],
),
);
}
@@ -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;
canvas.drawCircle(Offset(cx, cy), rPixel, curve1Paint);
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,7 +1078,9 @@ class _CartesianCanvasPainter extends CustomPainter {
path.lineTo(px, py);
}
}
canvas.drawPath(path, curve1Paint);
if (started) {
canvas.drawPath(path, curve1Paint);
}
}
// 4. Draw Curve 2 (Amber)
@@ -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;
canvas.drawLine(Offset(0, py1), Offset(size.width, py2), curve2Paint);
if (py1.isFinite && py2.isFinite) {
canvas.drawLine(Offset(0, py1), Offset(size.width, py2), curve2Paint);
}
} else {
// 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,7 +1118,9 @@ class _CartesianCanvasPainter extends CustomPainter {
path.lineTo(px, py);
}
}
canvas.drawPath(path, curve2Paint);
if (started) {
canvas.drawPath(path, curve2Paint);
}
}
// 5. Draw Intersection Points (Red Glow)
@@ -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);
File diff suppressed because it is too large Load Diff