Update Saqel Platform: 2026-09-13 16:31:17

This commit is contained in:
Hamza-Ayed
2026-09-13 16:31:18 +03:00
parent 81fa8c5dc8
commit 72c6575765
25 changed files with 6997 additions and 3852 deletions
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -28,51 +29,82 @@ class HistoryJordanChronologyLabView extends StatefulWidget {
class _HistoryJordanChronologyLabViewState
extends State<HistoryJordanChronologyLabView> {
int era = 2;
int era = 0;
double progress = 1.0;
// المحطات التاريخية الرسمية لمنهاج الصف العاشر (الوحدات 1-4)
static const milestones = [
{
'y': '1916',
't': 'الثورة العربية الكبرى',
'y': '550 ق.م',
't': 'تأسيس الدولة الأخمينية الفارسية',
'unit': 'الوحدة الأولى: الإمبراطورية الفارسية',
'd':
'انطلاق الثورة بقيادة الشريف الحسين بن علي ضد الحكم العثماني؛ تشكل الوعي العربي الحديث وخط سير الجيوش نحو الشام.',
'c': Color(0xFFFF9F0A)
'تأسيس الملك كورش الأكبر للإمبراطورية الأخمينية الفارسية، وتوحيد الميديين والفرس، وشق الطريق الملكي (2700 كم) من ساردس إلى سوسة وبرسبوليس.',
'c': Color(0xFFFFD60A),
'icon': '🏛️',
},
{
'y': '1921',
't': 'تأسيس إمارة شرق الأردن',
'y': '224 م',
't': 'تأسيس الدولة الساسانية (معركة هرمزجان)',
'unit': 'الوحدة الأولى: الإمبراطورية الفارسية',
'd':
'وصول الأمير عبد الله الأول إلى عمان (2 آذار) وتشكيل أول حكومة (11 نيسان) برئاسة رشيد طليع.',
'c': Color(0xFF8B5CF6)
'تتويج أردشير الأول شاهنشاهاً بعد معركة هرمزجان (224م) وإسقاط البارثيين، وإعلان طيسفون (المدائن) عاصمة جديدة وبناء إيوان كسرى ومجمع جنديسابور.',
'c': Color(0xFFFF9F0A),
'icon': '👑',
},
{
'y': '1928',
't': 'القانون الأساسي والمعاهدة',
'y': '651 م',
't': 'معركة نهاوند ونهاية الساسانيين',
'unit': 'الوحدة الأولى: الإمبراطورية الفارسية',
'd':
'أول وثيقة دستورية للإمارة (القانون الأساسي) وتنظيم العلاقة مع بريطانيا والمؤتمر الوطني الأول.',
'c': Color(0xFF60A5FA)
'معركة نهاوند (فتح الفتوح - 642م/651م) وسقوط يزدجرد الثالث، ودخول بلاد فارس بالكامل تحت راية الحضارة الإسلامية.',
'c': Color(0xFF30D158),
'icon': '⚔️',
},
{
'y': '1946',
't': 'الاستقلال 25 أيار',
'y': '1299 م',
't': 'تأسيس الدولة العثمانية',
'unit': 'الوحدة الثانية: الدولة العثمانية',
'd':
'إعلان الاستقلال التام ومبايعة الملك المؤسس ملكاً دستورياً وتحول الاسم إلى المملكة الأردنية الهاشمية.',
'c': Color(0xFF30D158)
'تأسيس الأمير عثمان بن أرطغرل للإمارة العثمانية في سوغوت شمال غرب الأناضول، وتوسعها التدريجي في آسيا الصغرى والبلقان.',
'c': Color(0xFF60A5FA),
'icon': '🏹',
},
{
'y': '1952',
't': 'دستور 1952',
'y': '1453 م',
't': 'فتح القسطنطينية (إسطنبول)',
'unit': 'الوحدة الثانية: الدولة العثمانية',
'd':
'صدور الدستور الأردني الحالي في عهد الملك طلال؛ ترسيخ الملكية الدستورية والحقوق والحريات.',
'c': Color(0xFF00F5D4)
'السلطان محمد الثاني (الفاتح) يفتح القسطنطينية ويُنهي الإمبراطورية البيزنطية ويجعلها عاصمة عثمانية، مستخدماً مدافع أوربان ونقل السفن براً.',
'c': Color(0xFF8B5CF6),
'icon': '🏰',
},
{
'y': '1994',
't': 'معاهدة وادي عربة',
'y': '1760 م',
't': 'انطلاق الثورة الصناعية',
'unit': 'الوحدة الثالثة: ثورات غيّرت العالم',
'd':
'معاهدة السلام الأردنية–الإسرائيلية؛ ملفات الحدود والمياه والأمن — تُدرس من الكتاب المقرر حصراً.',
'c': Color(0xFF0071E3)
'اختراع الآلة البخارية لجيمس واط وتطوير صناعة الغزل والنسيج في بريطانيا، والتحول الجذري من الإنتاج الزراعي واليدوي إلى المصانع والآلات.',
'c': Color(0xFF00F5D4),
'icon': '⚙️',
},
{
'y': '1789 م',
't': 'الثورة الفرنسية الكبرى',
'unit': 'الوحدة الثالثة: ثورات غيّرت العالم',
'd':
'سقوط سجن الباستيل في باريس، وإعلان حقوق الإنسان والمواطن، وإلغاء الإقطاع والمَلكية المطلقة، وإرساء مبادئ الحرية والعدالة والمساواة.',
'c': Color(0xFFFF375F),
'icon': '📜',
},
{
'y': '1805 م',
't': 'عصر محمد علي باشا وبناء الدولة الحديثة',
'unit': 'الوحدة الرابعة: شخصيات تاريخية أثرت في العالم',
'd':
'تولي محمد علي باشا حكم مصر عام 1805م، وبناء الجيش والأسطول الحديث، وإرسال البعثات التعليمية (رفاعة الطهطاوي)، وإنشاء مدرسة الألسن والقناطر الخيرية.',
'c': Color(0xFF10B981),
'icon': '🌟',
},
];
@@ -80,176 +112,257 @@ class _HistoryJordanChronologyLabViewState
Widget build(BuildContext context) {
final m = milestones[era];
return SaqelLabScaffold(
titleAr: 'التسلسل الزمني لتاريخ الأردن',
subtitleAr: 'اسحب المؤشر عبر المحطات — خريطة طريق + بطاقة الحدث',
titleAr: 'الخط الزمني الشامل لحضارات العالم — الصف العاشر',
subtitleAr:
'تسلسل منهجي يربط بين: الإمبراطورية الفارسية • الدولة العثمانية • الثورات الكبرى • الشخصيات المؤثرة',
identity: kHistoryJordanChronologyToolIdentity,
onCheckpointTriggered: widget.onCheckpointTriggered,
checkpointQuestion:
'اليوم الوطني لاستقلال المملكة الأردنية الهاشمية هو …',
'المعركة الفاصلة التي خاضها أردشير الأول وأسفرت عن تأسيس الدولة الساسانية عام 224م هي …',
checkpointOptions: const [
'25 أيار 1946',
'2 آذار 1921',
'16 نيسان 1928',
'10 حزيران 1916'
'معركة هرمزجان',
'معركة نهاوند',
'معركة الريدانية',
'معركة القادسية'
],
checkpointCorrectIdx: 0,
telemetry: [
LabPill('${m['y']} • ${m['t']}', color: m['c'] as Color),
LabPill('${m['icon']} ${m['y']}', color: m['c'] as Color),
LabPill('${m['t']}', color: Colors.white),
LabPill('${m['unit']}', color: AppColors.saqelCyan),
],
canvas: CustomPaint(
painter: _ChronoPainter(index: era, progress: progress),
painter: _WorldCivilizationsChronoPainter(index: era, milestones: milestones),
child: Container(),
),
controls: [
LabSlider(
label: 'المحطة التاريخية',
value: era.toDouble(),
min: 0,
max: 5,
display: '${milestones[era]['y']}',
onChanged: (v) => setState(() => era = v.round())),
label: 'المحطة التاريخية',
value: era.toDouble(),
min: 0,
max: (milestones.length - 1).toDouble(),
display: '${milestones[era]['y']}',
onChanged: (v) => setState(() => era = v.round()),
),
const SizedBox(height: 6),
SizedBox(
height: 40,
height: 42,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: milestones.length,
separatorBuilder: (_, __) => const SizedBox(width: 6),
itemBuilder: (ctx, i) {
final sel = i == era;
final item = milestones[i];
return GestureDetector(
onTap: () => setState(() => era = i),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: sel
? (milestones[i]['c'] as Color)
? (item['c'] as Color)
: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: sel ? Colors.white : Colors.white12,
width: sel ? 1.4 : 1.0,
),
),
alignment: Alignment.center,
child: Text(milestones[i]['y'] as String,
style: TextStyle(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(item['icon'] as String, style: const TextStyle(fontSize: 12)),
const SizedBox(width: 5),
Text(
item['y'] as String,
style: TextStyle(
color: sel ? Colors.black : Colors.white70,
fontWeight: FontWeight.w900,
fontSize: 12)),
fontSize: 11.5,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 8),
const SizedBox(height: 10),
LabFormulaCard(
title: '${m['y']} — ${m['t']}',
body: m['d'] as String,
title: '${m['icon']} ${m['y']} — ${m['t']}',
body: '${m['d']}\n\n• المنهج الوزاري: ${m['unit']}',
accent: m['c'] as Color,
),
],
footerNote:
'عرض صفي موجز؛ التفاصيل الدقيقة والوثائق من الكتاب المدرسي المقرر.',
'منهاج وزارة التربية والتعليم الأردنية • كتاب التاريخ للصف العاشر • الوحدات الأربع المقررة.',
);
}
}
class _ChronoPainter extends CustomPainter {
class _WorldCivilizationsChronoPainter extends CustomPainter {
final int index;
final double progress;
_ChronoPainter({required this.index, required this.progress});
final List<Map<String, dynamic>> milestones;
_WorldCivilizationsChronoPainter({
required this.index,
required this.milestones,
});
@override
void paint(Canvas c, Size s) {
c.drawRect(Offset.zero & s, Paint()..color = const Color(0xFF0A1220));
// simplified Jordan outline (schematic polygon, clearly stylised)
final mapRect = Rect.fromLTWH(18, 18, s.width * 0.40, s.height - 90);
c.drawRRect(RRect.fromRectAndRadius(mapRect, const Radius.circular(14)),
Paint()..color = const Color(0xFF0F2036));
final poly = [
const Offset(0.55, 0.08),
const Offset(0.80, 0.18),
const Offset(0.86, 0.42),
const Offset(0.70, 0.62),
const Offset(0.72, 0.88),
const Offset(0.40, 0.92),
const Offset(0.22, 0.66),
const Offset(0.30, 0.34),
]
.map((e) => Offset(mapRect.left + e.dx * mapRect.width,
mapRect.top + e.dy * mapRect.height))
.toList();
c.drawPath(Path()..addPolygon(poly, true),
Paint()..color = const Color(0xFF1E3A5F));
// revolt route dashed
final route = Path()
..moveTo(mapRect.left + mapRect.width * 0.72,
mapRect.top + mapRect.height * 0.88)
..quadraticBezierTo(
mapRect.left + mapRect.width * 0.5,
mapRect.top + mapRect.height * 0.5,
mapRect.left + mapRect.width * 0.6,
mapRect.top + mapRect.height * 0.15);
_dashed(c, route, const Color(0xFFFF9F0A));
final dotT = (index / 5).clamp(0.0, 1.0);
final dotPos = _pointOn(route, dotT, mapRect);
c.drawCircle(dotPos, 7, Paint()..color = const Color(0xFFFF375F));
c.drawCircle(
dotPos,
7,
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 1.6);
// timeline rail (right side)
final rx = s.width * 0.52, ry0 = 30.0, ry1 = s.height - 40;
c.drawLine(
Offset(rx, ry0),
Offset(rx, ry1),
Paint()
..color = Colors.white24
..strokeWidth = 3);
for (int i = 0; i < 6; i++) {
final y = ry0 + i * ((ry1 - ry0) / 5);
final sel = i == index;
c.drawCircle(Offset(rx, y), sel ? 11 : 7,
Paint()..color = sel ? const Color(0xFF00F5D4) : Colors.white24);
final w = s.width;
final h = s.height;
// Dark parchment starry background
final bgShader = const RadialGradient(
center: Alignment(0.0, -0.3),
colors: [Color(0xFF0F1B2C), Color(0xFF060B12)],
radius: 1.0,
).createShader(Rect.fromLTWH(0, 0, w, h));
c.drawRect(Rect.fromLTWH(0, 0, w, h), Paint()..shader = bgShader);
// Subtle historical coordinate lines
final gridPaint = Paint()
..color = Colors.white.withValues(alpha: 0.03)
..strokeWidth = 1.0;
for (double x = 0; x < w; x += 35) {
c.drawLine(Offset(x, 0), Offset(x, h), gridPaint);
}
// 4 Era Color Zones (أشرطة العصور التاريخية الأربعة)
final eraWidth = w / 4;
final eraLabels = [
'العصر الفارسي القديم\n(550 ق.م - 651 م)',
'العصر العثماني والفتوح\n(1299 م - 1923 م)',
'عصر الثورات الكبرى\n(1760 م - 1916 م)',
'بناء الدولة الحديثة\n(1805 م - 1938 م)',
];
final eraColors = [
const Color(0xFFFFD60A),
const Color(0xFF60A5FA),
const Color(0xFFFF375F),
const Color(0xFF10B981),
];
for (int i = 0; i < 4; i++) {
final x = i * eraWidth;
final zonePaint = Paint()
..color = eraColors[i].withValues(alpha: 0.04)
..style = PaintingStyle.fill;
c.drawRect(Rect.fromLTWH(x, 10, eraWidth - 2, h - 30), zonePaint);
final dividerPaint = Paint()
..color = Colors.white.withValues(alpha: 0.08)
..strokeWidth = 1.0;
c.drawLine(Offset(x + eraWidth - 2, 10), Offset(x + eraWidth - 2, h - 20), dividerPaint);
final tp = TextPainter(
text: TextSpan(
text: ['1916', '1921', '1928', '1946', '1952', '1994'][i],
style: TextStyle(
color: sel ? Colors.white : Colors.white54,
fontSize: 11,
fontWeight: FontWeight.w800)),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(c, Offset(rx + 18, y - 8));
text: eraLabels[i],
style: TextStyle(
color: eraColors[i].withValues(alpha: 0.7),
fontSize: 9.0,
fontWeight: FontWeight.w700,
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.rtl,
)..layout(maxWidth: eraWidth - 8);
tp.paint(c, Offset(x + (eraWidth - tp.width) / 2, 16));
}
}
void _dashed(Canvas c, Path p, Color col) {
final metrics = p.computeMetrics().toList();
for (final m in metrics) {
double d = 0;
while (d < m.length) {
c.drawPath(
m.extractPath(d, d + 8),
// Interactive Flowing S-Curve Timeline Rail (خط زمني انسيابي ممتد)
final railPath = Path();
final count = milestones.length;
final points = <Offset>[];
for (int i = 0; i < count; i++) {
final px = 24.0 + (i / (count - 1)) * (w - 48.0);
// Harmonious wave across the canvas
final py = (h * 0.52) + math.sin(i * 1.1) * (h * 0.16);
points.add(Offset(px, py));
}
railPath.moveTo(points.first.dx, points.first.dy);
for (int i = 0; i < points.length - 1; i++) {
final p0 = points[i];
final p1 = points[i + 1];
final mx = (p0.dx + p1.dx) / 2;
railPath.cubicTo(mx, p0.dy, mx, p1.dy, p1.dx, p1.dy);
}
// Draw main timeline track
c.drawPath(
railPath,
Paint()
..color = Colors.white24
..strokeWidth = 3.2
..style = PaintingStyle.stroke,
);
// Active illuminated segment
final activeColor = milestones[index]['c'] as Color;
final completedPath = Path()..moveTo(points.first.dx, points.first.dy);
for (int i = 0; i < index; i++) {
final p0 = points[i];
final p1 = points[i + 1];
final mx = (p0.dx + p1.dx) / 2;
completedPath.cubicTo(mx, p0.dy, mx, p1.dy, p1.dx, p1.dy);
}
c.drawPath(
completedPath,
Paint()
..color = activeColor
..strokeWidth = 3.8
..style = PaintingStyle.stroke,
);
// Draw milestone stations along the curve
for (int i = 0; i < count; i++) {
final pt = points[i];
final m = milestones[i];
final isSel = i == index;
final col = m['c'] as Color;
if (isSel) {
// Glowing halo
c.drawCircle(pt, 16, Paint()..color = col.withValues(alpha: 0.25));
c.drawCircle(
pt,
12,
Paint()
..color = col
..strokeWidth = 2.4);
d += 14;
..style = PaintingStyle.stroke
..strokeWidth = 2.0);
}
}
}
Offset _pointOn(Path route, double t, Rect mapRect) {
// approximate: lerp along bounding diagonal of route (classroom schematic)
return Offset(
mapRect.left + mapRect.width * (0.72 - 0.12 * t),
mapRect.top + mapRect.height * (0.88 - 0.73 * t),
);
// Station node
c.drawCircle(pt, isSel ? 7.5 : 5.0, Paint()..color = isSel ? Colors.white : col);
// Label (Year + Icon)
final tp = TextPainter(
text: TextSpan(
text: '${m['icon']} ${m['y']}',
style: TextStyle(
color: isSel ? Colors.white : Colors.white70,
fontSize: isSel ? 10.5 : 9.0,
fontWeight: isSel ? FontWeight.w900 : FontWeight.w700,
backgroundColor: isSel ? Colors.black87 : Colors.transparent,
),
),
textDirection: TextDirection.rtl,
)..layout();
final labelY = (i % 2 == 0) ? pt.dy - 24 : pt.dy + 12;
tp.paint(c, Offset(pt.dx - tp.width / 2, labelY));
}
}
@override
bool shouldRepaint(covariant _ChronoPainter o) => o.index != index;
bool shouldRepaint(covariant _WorldCivilizationsChronoPainter o) => o.index != index;
}
// ---------------------------------------------------------------------------
@@ -461,7 +574,7 @@ class _FlowPainter extends CustomPainter {
}
// ---------------------------------------------------------------------------
// 3) SORTING PLAYGROUND (Bubble vs Quick trace)
// 3) SORTING & SEARCHING PLAYGROUND (المصفوفات، الفرز، والبحث خطوة بخطوة)
// ---------------------------------------------------------------------------
class DigitalSortingLabView extends StatefulWidget {
final LabCheckpointCallback? onCheckpointTriggered;
@@ -474,52 +587,91 @@ class DigitalSortingLabView extends StatefulWidget {
class _DigitalSortingLabViewState extends State<DigitalSortingLabView>
with SingleTickerProviderStateMixin {
List<int> arr = [7, 2, 9, 4, 1, 6];
int algo = 0; // 0 bubble, 1 quick (trace)
int algo = 0; // 0: Bubble Sort, 1: Selection Sort, 2: Quick Sort
int stepIdx = 0;
List<List<int>> snaps = [];
List<String> notes = [];
bool playing = false;
late final AnimationController ctl;
List<List<int>> activeIndices = []; // [index1, index2, isSwapped]
bool isPlaying = false;
// ignore: unused_field
Timer? _autoPlayTimer;
@override
void initState() {
super.initState();
_buildTrace();
ctl = AnimationController(
vsync: this, duration: const Duration(milliseconds: 600))
..addListener(() {
if (!playing) return;
});
}
void _buildTrace() {
snaps = [List.of(arr)];
notes = ['البداية: ${arr.join(' ')}'];
notes = ['المصفوفة الأولية قبل البدء: [${arr.join(', ')}]'];
activeIndices = [[-1, -1, 0]];
if (algo == 0) {
// Bubble Sort with explicit comparison & swap frames
final a = List<int>.of(arr);
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a.length - 1 - i; j++) {
final n = a.length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - 1 - i; j++) {
// Comparison frame
snaps.add(List.of(a));
notes.add('مقارنة العنصر [j=$j]=${a[j]} مع [j+1=${j + 1}]=${a[j + 1]}');
activeIndices.add([j, j + 1, 0]);
if (a[j] > a[j + 1]) {
final t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
// Swap frame
snaps.add(List.of(a));
notes.add('بدّل ${a[j + 1]} و ${a[j]} → ${a.join(' ')}');
notes.add('تبديل ${a[j + 1]} مع ${a[j]} لأن ${a[j + 1]} > ${a[j]}');
activeIndices.add([j, j + 1, 1]);
}
}
}
} else if (algo == 1) {
// Selection Sort
final a = List<int>.of(arr);
final n = a.length;
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
snaps.add(List.of(a));
notes.add('البحث عن الأصغر: مقارنة [min=$minIdx]=${a[minIdx]} مع [j=$j]=${a[j]}');
activeIndices.add([minIdx, j, 0]);
if (a[j] < a[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
final t = a[i];
a[i] = a[minIdx];
a[minIdx] = t;
snaps.add(List.of(a));
notes.add('تبديل الأصغر ${a[i]} إلى موقعه الصحيح عند المؤشر [$i]');
activeIndices.add([i, minIdx, 1]);
}
}
} else {
// quicksort Lomuto trace (simplified, honest label)
// Quicksort Lomuto trace
final a = List<int>.of(arr);
void qs(int lo, int hi) {
if (lo >= hi) return;
final pivot = a[hi];
int i = lo;
for (int j = lo; j < hi; j++) {
snaps.add(List.of(a));
notes.add('مقارنة العنصر [j=$j]=${a[j]} مع المحور Pivot=$pivot');
activeIndices.add([j, hi, 0]);
if (a[j] < pivot) {
final t = a[i];
a[i] = a[j];
a[j] = t;
if (i != j) {
snaps.add(List.of(a));
notes.add('وضع العنصر الأصغر من المحور في القسم الأيسر: تبديل [$i] مع [$j]');
activeIndices.add([i, j, 1]);
}
i++;
}
}
@@ -527,49 +679,90 @@ class _DigitalSortingLabViewState extends State<DigitalSortingLabView>
a[i] = a[hi];
a[hi] = t;
snaps.add(List.of(a));
notes.add('محور $pivot → ${a.join(' ')}');
notes.add('تثبيت المحور $pivot في موقعه النهائي عند الفهرس [$i]');
activeIndices.add([i, hi, 1]);
qs(lo, i - 1);
qs(i + 1, hi);
}
qs(0, a.length - 1);
}
stepIdx = 0;
}
void _toggleAutoPlay() {
setState(() {
isPlaying = !isPlaying;
if (isPlaying) {
_autoPlayTimer = Timer.periodic(const Duration(milliseconds: 700), (timer) {
if (!mounted || !isPlaying) {
timer.cancel();
return;
}
if (stepIdx < snaps.length - 1) {
setState(() => stepIdx++);
} else {
setState(() => isPlaying = false);
timer.cancel();
}
});
} else {
_autoPlayTimer?.cancel();
}
});
}
@override
void dispose() {
ctl.dispose();
_autoPlayTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cur = snaps.isEmpty ? arr : snaps[stepIdx.clamp(0, snaps.length - 1)];
final active = activeIndices.isEmpty
? [-1, -1, 0]
: activeIndices[stepIdx.clamp(0, activeIndices.length - 1)];
final sorted = _isSorted(cur);
return SaqelLabScaffold(
titleAr: 'ملعب المصفوفات والفرز',
subtitleAr: 'قارن فقاعي O(n²) مع سريع O(n log n) خطوة بخطوة',
titleAr: 'معمل المصفوفات وخوارزميات الفرز التفاعلية',
subtitleAr:
'تتبع بصري حي مع إبراز مؤشرات المقارنة والتبديل وفهارس المصفوفة',
identity: kDigitalSortingToolIdentity,
onCheckpointTriggered: widget.onCheckpointTriggered,
checkpointQuestion: 'التعقيد الزمني الأسوأ للفرز الفقاعي (n عناصر) هو …',
checkpointQuestion: 'التعقيد الزمني الأسوأ للفرز الفقاعي (Bubble Sort) لمصفوفة حجمها n هو …',
checkpointOptions: const ['O(n²)', 'O(n)', 'O(log n)', 'O(1)'],
checkpointCorrectIdx: 0,
telemetry: [
LabPill(algo == 0 ? 'Bubble O(n²)' : 'Quick O(n log n)'),
LabPill('خطوة ${stepIdx + 1}/${snaps.length}'),
if (sorted) const LabPill('مرتبة ✓', color: Color(0xFF30D158)),
LabPill(
algo == 0
? 'فقاعي Bubble O(n²)'
: (algo == 1 ? 'اختيار Selection O(n²)' : 'سريع Quick O(n log n)'),
color: AppColors.saqelCyan,
),
LabPill('خطوة ${stepIdx + 1} من ${snaps.length}'),
if (sorted && stepIdx == snaps.length - 1)
const LabPill('مرتبة بنجاح ✓', color: Color(0xFF30D158)),
],
canvas: CustomPaint(
painter: _BarsPainter(values: cur),
painter: _EnhancedBarsPainter(
values: cur,
activeIdx1: active[0],
activeIdx2: active[1],
isSwap: active[2] == 1,
isFullySorted: sorted && stepIdx == snaps.length - 1,
),
child: Container(),
),
controls: [
LabSegments<int>(
labels: const ['فقاعي', 'سريع'],
values: const [0, 1],
labels: const ['فقاعي (Bubble)', 'اختيار (Selection)', 'سريع (Quick)'],
values: const [0, 1, 2],
current: algo,
onSelected: (v) => setState(() {
_autoPlayTimer?.cancel();
isPlaying = false;
algo = v;
_buildTrace();
}),
@@ -578,63 +771,115 @@ class _DigitalSortingLabViewState extends State<DigitalSortingLabView>
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: stepIdx > 0 ? () => setState(() => stepIdx--) : null,
child: OutlinedButton.icon(
onPressed: stepIdx > 0
? () {
_autoPlayTimer?.cancel();
setState(() {
isPlaying = false;
stepIdx--;
});
}
: null,
icon: const Icon(CupertinoIcons.chevron_right, size: 14),
label: const Text('السابق'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white24)),
child: const Text('◀ سابق'),
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white24),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: stepIdx < snaps.length - 1
? () => setState(() => stepIdx++)
: null,
child: ElevatedButton.icon(
onPressed: _toggleAutoPlay,
icon: Icon(isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_arrow_solid, size: 15),
label: Text(isPlaying ? 'إيقاف مؤقت' : 'تشغيل تلقائي ⏯️'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.saqelCyan,
foregroundColor: Colors.black),
child: const Text('التالي ▶',
style: TextStyle(fontWeight: FontWeight.w800)),
backgroundColor: isPlaying ? const Color(0xFFFF9F0A) : AppColors.saqelCyan,
foregroundColor: Colors.black,
textStyle: const TextStyle(fontWeight: FontWeight.w800),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: stepIdx < snaps.length - 1
? () {
_autoPlayTimer?.cancel();
setState(() {
isPlaying = false;
stepIdx++;
});
}
: null,
icon: const Icon(CupertinoIcons.chevron_left, size: 14),
label: const Text('التالي'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withValues(alpha: 0.12),
foregroundColor: Colors.white,
textStyle: const TextStyle(fontWeight: FontWeight.w700),
),
),
),
],
),
const SizedBox(height: 6),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(CupertinoIcons.shuffle, size: 15),
label: const Text('خلط المصفوفة'),
label: const Text('توليد مصفوفة عشوائية وخلط العناصر 🎲'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFFFD166),
side: const BorderSide(color: Color(0xFFFFD166))),
foregroundColor: const Color(0xFFFFD166),
side: const BorderSide(color: Color(0xFFFFD166)),
),
onPressed: () => setState(() {
_autoPlayTimer?.cancel();
isPlaying = false;
arr.shuffle(math.Random());
_buildTrace();
}),
),
),
const SizedBox(height: 8),
Directionality(
textDirection: TextDirection.ltr,
child: Text(
notes.isEmpty ? '' : notes[stepIdx.clamp(0, notes.length - 1)],
style: const TextStyle(
color: Color(0xFF00F5D4),
fontSize: 12,
fontFamily: 'monospace')),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.saqelCyan.withValues(alpha: 0.25)),
),
child: Row(
children: [
const Icon(CupertinoIcons.info_circle_fill, color: AppColors.saqelCyan, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
notes.isEmpty ? '' : notes[stepIdx.clamp(0, notes.length - 1)],
style: const TextStyle(
color: Color(0xFF00F5D4),
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
const SizedBox(height: 4),
const SizedBox(height: 6),
const LabFormulaCard(
title: 'لماذا السريع أسرع؟',
title: 'دليل الخوارزميات — الصف العاشر',
body:
'الفقاعي يقارن كل زوج مجاور (تربيعي). السريع يقسم حول محور ويفرز كل قسم (لوغاريتمي) — لاحظ قفزات المحور في التتبع.',
'• الفرز الفقاعي (Bubble): يقارن كل زوج متجاور ويبدلهما. تعقيد زمني O(n²).\n'
'• فرز الاختيار (Selection): يختار أصغر عنصر وينقله لموقعه النهائي. تعقيد زمني O(n²).\n'
'• الفرز السريع (Quick): يقسم المصفوفة حول محور (Pivot) ويفرز كل قسم لوغاريتمياً. تعقيد O(n log n).',
accent: AppColors.saqelCyan,
),
],
footerNote:
'تتبع تعليمي لمدخلات صغيرة؛ قياس الأداء الحقيقي يحتاج n كبيرة وتوقيتاً فعلياً.',
'منهاج وزارة التربية والتعليم الأردنية • المهارات الرقمية للصف العاشر • وحدة الخوارزميات والبرمجة.',
);
}
@@ -646,9 +891,20 @@ class _DigitalSortingLabViewState extends State<DigitalSortingLabView>
}
}
class _BarsPainter extends CustomPainter {
class _EnhancedBarsPainter extends CustomPainter {
final List<int> values;
_BarsPainter({required this.values});
final int activeIdx1;
final int activeIdx2;
final bool isSwap;
final bool isFullySorted;
_EnhancedBarsPainter({
required this.values,
required this.activeIdx1,
required this.activeIdx2,
required this.isSwap,
required this.isFullySorted,
});
@override
void paint(Canvas c, Size s) {
@@ -656,31 +912,82 @@ class _BarsPainter extends CustomPainter {
if (values.isEmpty) return;
final mx = values.reduce(math.max).toDouble();
final bw = s.width / values.length;
for (int i = 0; i < values.length; i++) {
final h = (values[i] / mx) * (s.height - 70);
final h = (values[i] / mx) * (s.height - 85);
final x = i * bw + 6;
final grad = const LinearGradient(
colors: [Color(0xFF00F5D4), Color(0xFF0071E3)],
begin: Alignment.bottomCenter,
end: Alignment.topCenter)
.createShader(Rect.fromLTWH(x, s.height - 30 - h, bw - 12, h));
final isComp = i == activeIdx1 || i == activeIdx2;
// Color coding for algorithm education
Color c1 = const Color(0xFF00F5D4);
Color c2 = const Color(0xFF0071E3);
if (isFullySorted) {
c1 = const Color(0xFF30D158);
c2 = const Color(0xFF10B981);
} else if (isComp) {
if (isSwap) {
c1 = const Color(0xFFFF375F);
c2 = const Color(0xFFFF5252);
} else {
c1 = const Color(0xFFFFD166);
c2 = const Color(0xFFFF9F0A);
}
}
final grad = LinearGradient(
colors: [c1, c2],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
).createShader(Rect.fromLTWH(x, s.height - 40 - h, bw - 12, h));
c.drawRRect(
RRect.fromLTRBR(x, s.height - 30 - h, x + bw - 12, s.height - 30,
const Radius.circular(7)),
Paint()..shader = grad);
RRect.fromLTRBR(
x,
s.height - 40 - h,
x + bw - 12,
s.height - 40,
const Radius.circular(8),
),
Paint()..shader = grad,
);
// Top value text
final tp = TextPainter(
text: TextSpan(
text: '${values[i]}',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w900)),
text: '${values[i]}',
style: TextStyle(
color: isComp ? Colors.white : Colors.white70,
fontSize: isComp ? 14 : 12.5,
fontWeight: FontWeight.w900,
),
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(c, Offset(x + (bw - 12 - tp.width) / 2, s.height - 30 - h - 20));
tp.paint(c, Offset(x + (bw - 12 - tp.width) / 2, s.height - 40 - h - 20));
// Bottom array index badge: [i]
final idxTp = TextPainter(
text: TextSpan(
text: '[$i]',
style: TextStyle(
color: isComp ? (isSwap ? const Color(0xFFFF375F) : const Color(0xFFFFD166)) : Colors.white38,
fontSize: 11,
fontWeight: isComp ? FontWeight.w800 : FontWeight.w600,
),
),
textDirection: TextDirection.ltr,
)..layout();
idxTp.paint(c, Offset(x + (bw - 12 - idxTp.width) / 2, s.height - 32));
}
}
@override
bool shouldRepaint(covariant _BarsPainter o) => o.values != values;
bool shouldRepaint(covariant _EnhancedBarsPainter o) =>
o.values != values ||
o.activeIdx1 != activeIdx1 ||
o.activeIdx2 != activeIdx2 ||
o.isSwap != isSwap ||
o.isFullySorted != isFullySorted;
}
File diff suppressed because it is too large Load Diff
@@ -587,7 +587,7 @@ const LabIdentity kFinanceBudgetToolIdentity = LabIdentity(
const LabIdentity kHistoryJordanChronologyToolIdentity = LabIdentity(
toolKey: 'history_jordan_chronology',
subjectAr: 'التاريخ',
lessonAr: 'التسلسل الزمني التفاعلي',
lessonAr: 'التسلسل الزمني للحضارات (العاشر)',
);
const LabIdentity kDigitalFlowchartToolIdentity = LabIdentity(
@@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/utils/app_logger.dart';
import '../../../data/models/socratic_checkpoint_model.dart';
import '../../widgets/socratic_dialog.dart';
import 'lab_identity.dart';
@@ -382,6 +383,9 @@ class Grade10LabsRegistry {
required String lessonTitle,
String? lessonId,
String? curriculumLessonId,
String? filePath,
String? unitKey,
String? semesterKey,
bool? allowPreview,
}) {
final usePreview = allowPreview ?? previewModeEnabled;
@@ -392,47 +396,110 @@ class Grade10LabsRegistry {
return null;
}
if (usePreview) {
// 1. Exact match on curriculumLessonId within candidate subject labs
if (curriculumLessonId != null && curriculumLessonId.isNotEmpty) {
for (final c in candidates) {
if (c.identity.curriculumLessonId == curriculumLessonId) {
return c;
}
}
}
// 2. Lesson Title fuzzy matching strictly within candidate subject labs
final t = lessonTitle.toLowerCase().trim();
// 1. Exact match on sourceMarkdown / filePath (Highest precision, 100% deterministic)
if (filePath != null && filePath.isNotEmpty) {
final cleanPath = filePath.replaceAll('\\', '/').trim();
for (final c in candidates) {
final ct = c.lessonAr.toLowerCase().trim();
if (t == ct || (ct.length > 4 && (t.contains(ct) || ct.contains(t)))) {
return c;
}
}
// 3. Lesson Key / Tool Key match strictly within candidate subject labs
if (lessonId != null && lessonId.isNotEmpty) {
for (final c in candidates) {
if (c.identity.lessonKey == lessonId ||
c.identity.toolKey == lessonId ||
c.identity.curriculumLessonId.endsWith(lessonId) ||
c.identity.sourceMarkdown.contains(lessonId)) {
if (c.identity.sourceMarkdown.isNotEmpty &&
(c.identity.sourceMarkdown == cleanPath ||
cleanPath.endsWith(c.identity.sourceMarkdown) ||
c.identity.sourceMarkdown.endsWith(cleanPath))) {
if (usePreview || c.identity.isPublished) {
AppLogger.event('VirtualLabResolvedByPath', details: {
'subject': subjectTitle,
'lesson': lessonTitle,
'path': cleanPath,
'lab': c.lessonAr,
}, tag: 'LAB_REGISTRY');
return c;
}
}
}
return null;
}
// 2. Synthesized canonical slug match from filePath (e.g. math_10_semester_1_unit_01_lesson_02)
if (filePath != null && filePath.isNotEmpty) {
final slug = filePath
.replaceAll('.md', '')
.replaceAll('grade_10/', '')
.replaceAll('/', '_')
.trim();
for (final c in candidates) {
if (c.identity.curriculumLessonId == slug) {
if (usePreview || c.identity.isPublished) {
AppLogger.event('VirtualLabResolvedBySlug', details: {
'subject': subjectTitle,
'lesson': lessonTitle,
'slug': slug,
'lab': c.lessonAr,
}, tag: 'LAB_REGISTRY');
return c;
}
}
}
}
// 3. Exact match on curriculumLessonId
if (curriculumLessonId != null && curriculumLessonId.isNotEmpty) {
for (final c in candidates) {
if (c.identity.curriculumLessonId == curriculumLessonId && c.identity.isPublished) {
return c;
if (c.identity.curriculumLessonId == curriculumLessonId) {
if (usePreview || c.identity.isPublished) {
AppLogger.event('VirtualLabResolvedByCurriculumId', details: {
'subject': subjectTitle,
'lesson': lessonTitle,
'curriculumId': curriculumLessonId,
'lab': c.lessonAr,
}, tag: 'LAB_REGISTRY');
return c;
}
}
}
}
if (usePreview) {
// 4. Strict Title matching within candidate subject labs (only within same unit if provided)
final t = lessonTitle.toLowerCase().trim();
for (final c in candidates) {
if (unitKey != null && c.identity.unitKey.isNotEmpty && c.identity.unitKey != unitKey) {
continue;
}
if (semesterKey != null && c.identity.semesterKey.isNotEmpty && c.identity.semesterKey != semesterKey) {
continue;
}
final ct = c.lessonAr.toLowerCase().trim();
if (t == ct || (ct.length > 5 && (t.contains(ct) || ct.contains(t)))) {
AppLogger.event('VirtualLabResolvedByTitle', details: {
'subject': subjectTitle,
'lesson': lessonTitle,
'match': ct,
'lab': c.lessonAr,
}, tag: 'LAB_REGISTRY');
return c;
}
}
// 5. Lesson Key match strictly constrained to matching unit & semester
if (lessonId != null && lessonId.isNotEmpty) {
for (final c in candidates) {
if (unitKey != null && c.identity.unitKey.isNotEmpty && c.identity.unitKey != unitKey) {
continue;
}
if (semesterKey != null && c.identity.semesterKey.isNotEmpty && c.identity.semesterKey != semesterKey) {
continue;
}
if (c.identity.lessonKey == lessonId || c.identity.toolKey == lessonId) {
AppLogger.event('VirtualLabResolvedByKey', details: {
'subject': subjectTitle,
'lesson': lessonTitle,
'key': lessonId,
'lab': c.lessonAr,
}, tag: 'LAB_REGISTRY');
return c;
}
}
}
}
return null;
}
@@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/utils/app_logger.dart';
import '../../../data/models/socratic_checkpoint_model.dart';
import '../../widgets/socratic_dialog.dart';
import 'labs_gallery_screen.dart';
@@ -17,6 +18,7 @@ class SubjectVirtualLabsView extends StatefulWidget {
final Color primaryColor;
final Widget? specializedToolWidget;
final String? specializedToolTitle;
final String? selectedSemester;
const SubjectVirtualLabsView({
super.key,
@@ -24,6 +26,7 @@ class SubjectVirtualLabsView extends StatefulWidget {
required this.primaryColor,
this.specializedToolWidget,
this.specializedToolTitle,
this.selectedSemester,
});
@override
@@ -34,7 +37,26 @@ class _SubjectVirtualLabsViewState extends State<SubjectVirtualLabsView> {
int _selectedIndex = 0;
bool _showSpecialized = false;
@override
void didUpdateWidget(covariant SubjectVirtualLabsView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedSemester != widget.selectedSemester) {
AppLogger.event('VirtualLabsSemesterChanged', details: {
'subject': widget.subjectTitle,
'from': oldWidget.selectedSemester,
'to': widget.selectedSemester,
}, tag: 'VIRTUAL_LABS_VIEW');
setState(() {
_selectedIndex = 0;
_showSpecialized = false;
});
}
}
void _openGallery(BuildContext context) {
AppLogger.event('OpenVirtualLabsGallery', details: {
'subject': widget.subjectTitle,
}, tag: 'VIRTUAL_LABS_VIEW');
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => Scaffold(
@@ -52,6 +74,12 @@ class _SubjectVirtualLabsViewState extends State<SubjectVirtualLabsView> {
void _triggerCheckpoint(
String q, List<String> opts, int correct, String lessonTitle) {
AppLogger.event('LabCheckpointTriggered', details: {
'lesson': lessonTitle,
'question': q,
'optionsCount': opts.length,
'correctIndex': correct,
}, tag: 'VIRTUAL_LABS_VIEW');
final checkpoint = SocraticCheckpointModel(
id: lessonTitle.hashCode & 0x7fffffff,
questionText: q,
@@ -79,10 +107,24 @@ class _SubjectVirtualLabsViewState extends State<SubjectVirtualLabsView> {
Widget build(BuildContext context) {
final allLabs =
Grade10LabsRegistry.bySubjectNormalized(widget.subjectTitle);
final labs = Grade10LabsRegistry.previewModeEnabled
var candidateLabs = Grade10LabsRegistry.previewModeEnabled
? allLabs
: allLabs.where((e) => e.identity.isPublished).toList();
if (widget.selectedSemester != null && widget.selectedSemester!.isNotEmpty) {
final semesterLabs = candidateLabs.where((e) {
if (e.identity.semesterKey.isNotEmpty) {
return e.identity.semesterKey == widget.selectedSemester;
}
return true; // Keep standalone authoring tools accessible
}).toList();
if (semesterLabs.isNotEmpty) {
candidateLabs = semesterLabs;
}
}
final labs = candidateLabs;
if (labs.isEmpty) {
if (widget.specializedToolWidget != null) {
return widget.specializedToolWidget!;
@@ -228,7 +270,13 @@ class _SubjectVirtualLabsViewState extends State<SubjectVirtualLabsView> {
idx == labs.length) {
final isSel = _showSpecialized;
return InkWell(
onTap: () => setState(() => _showSpecialized = true),
onTap: () {
AppLogger.event('SelectSpecializedTool', details: {
'subject': widget.subjectTitle,
'tool': widget.specializedToolTitle,
}, tag: 'VIRTUAL_LABS_VIEW');
setState(() => _showSpecialized = true);
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(
@@ -263,6 +311,13 @@ class _SubjectVirtualLabsViewState extends State<SubjectVirtualLabsView> {
final isSel = !_showSpecialized && activeIndex == idx;
return InkWell(
onTap: () {
AppLogger.event('SelectVirtualLabChip', details: {
'subject': widget.subjectTitle,
'index': idx,
'lab': lab.lessonAr,
'curriculumId': lab.identity.curriculumLessonId,
'semester': lab.identity.semesterKey,
}, tag: 'VIRTUAL_LABS_VIEW');
setState(() {
_showSpecialized = false;
_selectedIndex = idx;