Update Saqel Platform: 2026-09-13 16:31:17
This commit is contained in:
+514
-207
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+57
-2
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user