Update Saqel Platform: 2026-09-05 00:11:09
This commit is contained in:
+146
-14
@@ -8,21 +8,22 @@ import '../../../data/datasources/curriculum_baked_data.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
import '../../widgets/english_tts_player_widget.dart';
|
||||
import 'physics_interactive_lab_view.dart';
|
||||
import 'math_interactive_lab_view.dart';
|
||||
import 'english_interactive_lab_view.dart';
|
||||
|
||||
/**
|
||||
* ==============================================================================
|
||||
* SAQEL ENTERPRISE (EDTECH 2.0) - CURRICULUM DOCUMENT & TEXTBOOK VIEWER
|
||||
* ==============================================================================
|
||||
*
|
||||
* ملف: curriculum_document_viewer_screen.dart
|
||||
* الهدف المعماري:
|
||||
* استعراض الكتب المدرسية المقررة والمذكرات الوزارية وأوراق العمل محلياً وسحابياً:
|
||||
* 1. دعم التخزين المحلي (Offline Storage): قراءة وتصفح دائم دون اتصال بعد الحفظ.
|
||||
* 2. محتوى وزاري مخبوز وأصيل (CurriculumBakedData) للمواد الثلاث (رياضيات 10، فيزياء 10، إنجليزي 10).
|
||||
* 3. دعم الناطق الصوتي الذكي (English TTS) المدمج لقراءة النصوص والمفردات.
|
||||
* 4. ربط فوري بالمختبر التفاعلي (Virtual Lab) لمفاهيم المتجهات والفيزياء.
|
||||
* 5. التحكم بحجم الخط وزر الحفظ في الجهاز.
|
||||
*/
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE (EDTECH 2.0) - CURRICULUM DOCUMENT & TEXTBOOK VIEWER
|
||||
/// ==============================================================================
|
||||
///
|
||||
/// ملف: curriculum_document_viewer_screen.dart
|
||||
/// الهدف المعماري:
|
||||
/// استعراض الكتب المدرسية المقررة والمذكرات الوزارية وأوراق العمل محلياً وسحابياً:
|
||||
/// 1. دعم التخزين المحلي (Offline Storage): قراءة وتصفح دائم دون اتصال بعد الحفظ.
|
||||
/// 2. محتوى وزاري مخبوز وأصيل (CurriculumBakedData) للمواد الثلاث (رياضيات 10، فيزياء 10، إنجليزي 10).
|
||||
/// 3. دعم الناطق الصوتي الذكي (English TTS) المدمج لقراءة النصوص والمفردات.
|
||||
/// 4. ربط فوري بالمختبر التفاعلي (Virtual Lab) لمفاهيم المتجهات والفيزياء.
|
||||
/// 5. التحكم بحجم الخط وزر الحفظ في الجهاز.
|
||||
///
|
||||
class CurriculumDocumentViewerScreen extends StatefulWidget {
|
||||
final String title;
|
||||
final String documentType; // 'worksheet', 'summary', 'textbook', 'lesson'
|
||||
@@ -63,6 +64,13 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
|
||||
widget.subjectTitle.contains('فيزياء') ||
|
||||
(widget.subjectId ?? '').toLowerCase().contains('physic');
|
||||
|
||||
bool get _isMath =>
|
||||
widget.subjectTitle.contains('رياضيات') ||
|
||||
(widget.subjectId ?? '').toLowerCase().contains('math') ||
|
||||
widget.title.contains('معادلات') ||
|
||||
widget.title.contains('جيوجبرا') ||
|
||||
(widget.filePath ?? '').toLowerCase().contains('geogebra');
|
||||
|
||||
String get _effectiveSubjectId => widget.subjectId ?? (_isEnglish ? 'english_10' : (_isPhysics ? 'physics_10' : 'math_10'));
|
||||
String get _effectiveFilePath => widget.filePath ?? widget.title;
|
||||
|
||||
@@ -266,6 +274,130 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Direct Math GeoGebra Lab Cross-Link
|
||||
if (_isMath) ...[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
title: const Text('معمل برمجية جيوجبرا 📐', style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: const MathInteractiveLabView(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0F2027), Color(0xFF203A43), Color(0xFF2C5364)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.saqelCyan.withAlpha(40),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.function, color: AppColors.saqelCyan, size: 26),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'معمل برمجية جيوجبرا التفاعلي (GeoGebra Lab) 📐',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 14),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'حل أنظمة المعادلات بيانياً وكشف نقاط التقاطع وحساب المميز (تطبيق نشاط ص 16-17)',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 11.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(CupertinoIcons.arrow_left, color: AppColors.saqelCyan, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Direct English Interactive Lab Cross-Link
|
||||
if (_isEnglish) ...[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
title: const Text('مختبر اللغة الإنجليزية (Action Pack 10) 🎧', style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: const EnglishInteractiveLabView(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF141E30), Color(0xFF243B55)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.appleBlue.withAlpha(90)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.appleBlue.withAlpha(40),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.headphones, color: AppColors.appleBlue, size: 26),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'مختبر الصوتيات والقواعد التفاعلي (English Lab) 🎧',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 14),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'استمع للنطق الصوتي النقي (IPA) وتدرب على محاكي القواعد والأزمنة وبنك المفردات الذكي',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 11.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(CupertinoIcons.arrow_left, color: AppColors.appleBlue, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Direct Physics Virtual Lab Cross-Link
|
||||
if (_isPhysics) ...[
|
||||
GestureDetector(
|
||||
|
||||
+974
@@ -0,0 +1,974 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE - ACTION PACK 10 ENGLISH INTERACTIVE LAB
|
||||
/// ==============================================================================
|
||||
///
|
||||
/// مختبر اللغة الإنجليزية التفاعلي (منهاج Action Pack 10 الوزاري المعتمد):
|
||||
/// 1. محاكي القواعد التفاعلي (Grammar & Tenses Engine: Present Perfect, Modals, Relative Clauses).
|
||||
/// 2. استوديو الصوتيات والنطق الحي بالذكاء الاصطناعي مع رموز IPA ومحرك flutter_tts.
|
||||
/// 3. بنك المفردات والمتلازمات اللفظية (Collocations & Vocabulary Bank) للوحدات 1 إلى 5.
|
||||
class EnglishInteractiveLabView extends StatefulWidget {
|
||||
final String? initialTopic;
|
||||
|
||||
const EnglishInteractiveLabView({
|
||||
super.key,
|
||||
this.initialTopic,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EnglishInteractiveLabView> createState() =>
|
||||
_EnglishInteractiveLabViewState();
|
||||
}
|
||||
|
||||
class _EnglishInteractiveLabViewState extends State<EnglishInteractiveLabView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final FlutterTts _tts = FlutterTts();
|
||||
bool _isPlayingAudio = false;
|
||||
|
||||
// Grammar Simulator State
|
||||
int _selectedGrammarTopicIndex = 0; // 0 = Present Perfect vs Past Simple, 1 = Modals, 2 = Relative Clauses
|
||||
|
||||
// Phonetics & Vocabulary State
|
||||
int _selectedVocabUnitIndex = 0;
|
||||
int _selectedWordIndex = 0;
|
||||
|
||||
final List<Map<String, dynamic>> _grammarTopics = [
|
||||
{
|
||||
'title': 'Present Perfect vs. Past Simple (الوحدة 1 و 2)',
|
||||
'badge': 'Tenses & Timeline',
|
||||
'concept':
|
||||
'Present Perfect connects a past action to the present, or expresses life experiences without a specified time. Past Simple describes finished actions at a definite past time.',
|
||||
'formula_present_perfect': 'Subject + have / has + Past Participle (V3)',
|
||||
'formula_past_simple': 'Subject + Past Form (V2)',
|
||||
'time_markers_present': ['already', 'yet', 'just', 'ever', 'never', 'since 2018', 'for 5 years'],
|
||||
'time_markers_past': ['yesterday', 'in 2021', 'last week', 'two days ago', 'when I was young'],
|
||||
'pairs': [
|
||||
{
|
||||
'present': 'Scientists have developed autonomous exploration drones.',
|
||||
'present_ar': 'طور العلماء طائرات استكشافية مسيّرة (والنتيجة قائمة ومستمرة الآن).',
|
||||
'past': 'Voyager 1 was launched in 1977.',
|
||||
'past_ar': 'أُطلق المسبار فوياجر 1 عام 1977 (حدث منتهٍ في زمن ماضٍ محدد).',
|
||||
},
|
||||
{
|
||||
'present': 'He has lived in Amman for ten years.',
|
||||
'present_ar': 'عاش في عمّان لمدة 10 سنوات (وما زال يسكن فيها حتى اليوم).',
|
||||
'past': 'He lived in Irbid for two years when he was a child.',
|
||||
'past_ar': 'عاش في إربد لسنتين عندما كان طفلاً (وانتهى ذلك الزمن).',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'title': 'Modals of Deduction & Obligation (الوحدة 4)',
|
||||
'badge': 'Modals of Deduction',
|
||||
'concept':
|
||||
'We use "must" when we are almost 100% sure something is true. We use "can\'t" when we are sure it is impossible. We use "might / could" for possibility.',
|
||||
'formula_present_perfect': 'must / might / can\'t + Base Verb (Infinitive)',
|
||||
'formula_past_simple': 'must / might / can\'t + have + V3 (Past Deduction)',
|
||||
'time_markers_present': ['must (sure it is true)', 'can\'t (sure it is impossible)', 'might / may (possible)'],
|
||||
'time_markers_past': ['have to / must (obligation)', 'don\'t have to (lack of obligation)', 'mustn\'t (prohibition)'],
|
||||
'pairs': [
|
||||
{
|
||||
'present': 'She has been traveling all night; she must be exhausted.',
|
||||
'present_ar': 'سافرت طوال الليل؛ لا بد أنها مرهقة للغاية (استنتاج مؤكد 100%).',
|
||||
'past': 'He can\'t be the pilot; he is too young.',
|
||||
'past_ar': 'مستحيل أن يكون هو الطيار؛ فهو صغير جداً في السن (استنتاج مستحيل).',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'title': 'Defining & Non-Defining Relative Clauses (الوحدة 4 و 5)',
|
||||
'badge': 'Relative Pronouns',
|
||||
'concept':
|
||||
'Relative clauses give extra information about a noun. Use "who" for people, "which/that" for things, "where" for places, and "whose" for possession.',
|
||||
'formula_present_perfect': 'Noun + [who / which / where / whose] + Clause',
|
||||
'formula_past_simple': 'Non-defining clauses require commas: , which ... ,',
|
||||
'time_markers_present': ['who (people)', 'which / that (objects & ideas)', 'where (places)'],
|
||||
'time_markers_past': ['whose (possession)', 'when (time)', 'why (reasons)'],
|
||||
'pairs': [
|
||||
{
|
||||
'present': 'Petra is a historical city which attracts thousands of tourists every week.',
|
||||
'present_ar': 'البتراء مدينة تاريخية تجذب آلاف السياح أسبوعياً.',
|
||||
'past': 'An architect is a professional who designs eco-friendly buildings.',
|
||||
'past_ar': 'المهندس المعماري هو المحترف الذي يصمم المباني الصديقة للبيئة.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
final List<Map<String, dynamic>> _vocabUnits = [
|
||||
{
|
||||
'unit': 'Unit 1: Looking Good',
|
||||
'words': [
|
||||
{
|
||||
'word': 'Appearance',
|
||||
'ipa': '/əˈpɪə.rəns/',
|
||||
'pos': 'noun',
|
||||
'ar': 'المظهر الخارجي / الهيئة',
|
||||
'def': 'The way that someone or something looks.',
|
||||
'sentence': 'First impressions are often influenced by personal appearance and body language.',
|
||||
'collocations': 'Physical appearance, personal appearance, deceptive appearance',
|
||||
},
|
||||
{
|
||||
'word': 'Accidental',
|
||||
'ipa': '/ˌæk.sɪˈden.təl/',
|
||||
'pos': 'adjective',
|
||||
'ar': 'عَرَضي / غير مقصود / بالصدفة',
|
||||
'def': 'Happening by chance, not planned or intended.',
|
||||
'sentence': 'The discovery of the ancient mosaic in Madaba was completely accidental.',
|
||||
'collocations': 'Accidental discovery, accidental damage',
|
||||
},
|
||||
{
|
||||
'word': 'Confident',
|
||||
'ipa': '/ˈkɒn.fɪ.dənt/',
|
||||
'pos': 'adjective',
|
||||
'ar': 'واثق من نفسه',
|
||||
'def': 'Feeling sure about your abilities or qualities.',
|
||||
'sentence': 'Wearing formal attire can make candidates feel more confident in job interviews.',
|
||||
'collocations': 'Self-confident, feel confident, highly confident',
|
||||
},
|
||||
{
|
||||
'word': 'Casual',
|
||||
'ipa': '/ˈkæʒ.u.əl/',
|
||||
'pos': 'adjective',
|
||||
'ar': 'عفوي / غير رسمي (ملابس مريحة)',
|
||||
'def': 'Relaxed and unconcerned; not formal.',
|
||||
'sentence': 'On casual days, university students prefer lightweight cotton clothes.',
|
||||
'collocations': 'Casual clothes, casual wear, casual observer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'unit': 'Unit 2: The Digital Mind',
|
||||
'words': [
|
||||
{
|
||||
'word': 'Artificial Intelligence',
|
||||
'ipa': '/ˌɑː.tɪˈfɪʃ.əl ɪnˈtel.ɪ.dʒəns/',
|
||||
'pos': 'noun phrase',
|
||||
'ar': 'الذكاء الاصطناعي',
|
||||
'def': 'The development of computer systems able to perform tasks that normally require human intelligence.',
|
||||
'sentence': 'Saqel platform uses artificial intelligence to adapt learning roadmaps for each student.',
|
||||
'collocations': 'Generative artificial intelligence, AI algorithms, AI ethics',
|
||||
},
|
||||
{
|
||||
'word': 'Algorithm',
|
||||
'ipa': '/ˈæl.ɡə.rɪ.ðəm/',
|
||||
'pos': 'noun',
|
||||
'ar': 'خوارزمية حسابية',
|
||||
'def': 'A set of mathematical instructions used by computers to solve problems or make calculations.',
|
||||
'sentence': 'Search engine algorithms rank web pages based on relevance and authority.',
|
||||
'collocations': 'Complex algorithm, optimization algorithm, search algorithm',
|
||||
},
|
||||
{
|
||||
'word': 'Voyager',
|
||||
'ipa': '/ˈvɔɪ.ɪ.dʒər/',
|
||||
'pos': 'noun',
|
||||
'ar': 'مستكشف الفضاء / مسافر في رحلة طويلة',
|
||||
'def': 'A person who goes on a long journey, or the legendary NASA interstellar spacecraft.',
|
||||
'sentence': 'Voyager 1 carries a golden record containing sounds and images of planet Earth.',
|
||||
'collocations': 'Space voyager, daring voyager',
|
||||
},
|
||||
{
|
||||
'word': 'Breakthrough',
|
||||
'ipa': '/ˈbreɪk.θruː/',
|
||||
'pos': 'noun',
|
||||
'ar': 'إنجاز تاريخي غير مسبوق / قفزة نوعية',
|
||||
'def': 'An important discovery or event that helps improve a situation or provide an answer.',
|
||||
'sentence': 'Quantum computing represents a major technological breakthrough in modern science.',
|
||||
'collocations': 'Major breakthrough, scientific breakthrough, medical breakthrough',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'unit': 'Unit 3: Active and Healthy',
|
||||
'words': [
|
||||
{
|
||||
'word': 'Endurance',
|
||||
'ipa': '/ɪnˈdʒʊə.rəns/',
|
||||
'pos': 'noun',
|
||||
'ar': 'قوة التحمل والصبر البدني',
|
||||
'def': 'The ability to keep doing something difficult, unpleasant, or painful for a long time.',
|
||||
'sentence': 'Long-distance marathon runners need exceptional physical endurance and cardiovascular stamina.',
|
||||
'collocations': 'Physical endurance, endurance training, test of endurance',
|
||||
},
|
||||
{
|
||||
'word': 'Mediterranean Diet',
|
||||
'ipa': '/ˌmed.ɪ.tərˈeɪ.ni.ən ˈdaɪ.ət/',
|
||||
'pos': 'noun phrase',
|
||||
'ar': 'حمية حوض البحر الأبيض المتوسط',
|
||||
'def': 'A diet typical of Mediterranean countries, rich in olive oil, fish, grains, and vegetables.',
|
||||
'sentence': 'Nutritionists recommend the Mediterranean diet for heart health and longevity.',
|
||||
'collocations': 'Adopt a Mediterranean diet, heart-healthy diet',
|
||||
},
|
||||
{
|
||||
'word': 'Stamina',
|
||||
'ipa': '/ˈstæm.ɪ.nə/',
|
||||
'pos': 'noun',
|
||||
'ar': 'اللياقة العالية والجلد البدني',
|
||||
'def': 'The physical or mental energy that allows you to do something for a long time.',
|
||||
'sentence': 'Regular aerobic exercises build stamina and reduce fatigue.',
|
||||
'collocations': 'Build stamina, athletic stamina, mental stamina',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'unit': 'Unit 4: Time to Move',
|
||||
'words': [
|
||||
{
|
||||
'word': 'Destination',
|
||||
'ipa': '/ˌdes.tɪˈneɪ.ʃən/',
|
||||
'pos': 'noun',
|
||||
'ar': 'الوجهة السياحية / المقصد',
|
||||
'def': 'The place where someone is going or where something is being sent or taken.',
|
||||
'sentence': 'Wadi Rum is one of the most famous tourist destinations in Jordan.',
|
||||
'collocations': 'Holiday destination, popular destination, final destination',
|
||||
},
|
||||
{
|
||||
'word': 'Locomotive',
|
||||
'ipa': '/ˌləʊ.kəˈməʊ.tɪv/',
|
||||
'pos': 'noun',
|
||||
'ar': 'قاطرة قطار سريعة',
|
||||
'def': 'The engine of a train that pulls it along railway tracks.',
|
||||
'sentence': 'High-speed electric locomotives produce fewer carbon emissions than traditional diesel engines.',
|
||||
'collocations': 'Steam locomotive, electric locomotive, modern locomotive',
|
||||
},
|
||||
{
|
||||
'word': 'Emission',
|
||||
'ipa': '/iˈmɪʃ.ən/',
|
||||
'pos': 'noun',
|
||||
'ar': 'انبعاثات غازية وكربونية',
|
||||
'def': 'An amount of gas, heat, or light that is sent out.',
|
||||
'sentence': 'Public transport networks help reduce harmful greenhouse gas emissions in crowded cities.',
|
||||
'collocations': 'Zero emissions, carbon emissions, harmful emissions',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'unit': 'Unit 5: The Next Step',
|
||||
'words': [
|
||||
{
|
||||
'word': 'Apprenticeship',
|
||||
'ipa': '/əˈpren.tɪs.ʃɪp/',
|
||||
'pos': 'noun',
|
||||
'ar': 'برنامج التدريب المهني والعملي',
|
||||
'def': 'A period of time working for an employer in return for instruction in a trade or craft.',
|
||||
'sentence': 'Vocational students gain hands-on engineering skills through a three-year apprenticeship.',
|
||||
'collocations': 'Serve an apprenticeship, vocational apprenticeship, completed apprenticeship',
|
||||
},
|
||||
{
|
||||
'word': 'Ambitious',
|
||||
'ipa': '/æmˈbɪʃ.əs/',
|
||||
'pos': 'adjective',
|
||||
'ar': 'طموح / متطلع للإنجاز العالي',
|
||||
'def': 'Having a strong wish to be successful, powerful, or rich.',
|
||||
'sentence': 'Tawjihi students have ambitious goals to specialize in modern tech disciplines.',
|
||||
'collocations': 'Ambitious young professional, highly ambitious, ambitious project',
|
||||
},
|
||||
{
|
||||
'word': 'Internship',
|
||||
'ipa': '/ˈɪn.tɜːn.ʃɪp/',
|
||||
'pos': 'noun',
|
||||
'ar': 'فترة تدريب وظيفي ميداني',
|
||||
'def': 'A period of time during which a student or new graduate works to get experience in a profession.',
|
||||
'sentence': 'She completed a summer internship at a leading financial firm in Amman.',
|
||||
'collocations': 'Paid internship, summer internship, clinical internship',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_initTts();
|
||||
}
|
||||
|
||||
Future<void> _initTts() async {
|
||||
try {
|
||||
await _tts.setLanguage('en-GB');
|
||||
await _tts.setSpeechRate(0.45); // Clear, articulate educational pace
|
||||
await _tts.setPitch(1.0);
|
||||
_tts.setCompletionHandler(() {
|
||||
if (mounted) setState(() => _isPlayingAudio = false);
|
||||
});
|
||||
_tts.setErrorHandler((_) {
|
||||
if (mounted) setState(() => _isPlayingAudio = false);
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _speakWord(String text) async {
|
||||
if (text.isEmpty) return;
|
||||
setState(() {
|
||||
_isPlayingAudio = true;
|
||||
});
|
||||
try {
|
||||
await _tts.stop();
|
||||
await _tts.speak(text);
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _isPlayingAudio = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tts.stop();
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Column(
|
||||
children: [
|
||||
// Sub-navigation bar
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.headphones, color: AppColors.saqelCyan, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'مختبر اللغة الإنجليزية (Action Pack 10) — الصوتيات والقواعد والمفردات',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.appleBlue.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.appleBlue.withAlpha(90)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.speaker_2_fill, color: AppColors.appleBlue, size: 12),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'صوتيات ذكية (IPA & TTS)',
|
||||
style: TextStyle(color: AppColors.appleBlue, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Main Tab selector
|
||||
Container(
|
||||
color: AppColors.darkSurface,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: AppColors.saqelCyan,
|
||||
indicatorWeight: 3,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: AppColors.textSecondaryDark,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
tabs: const [
|
||||
Tab(icon: Icon(CupertinoIcons.wand_rays_inverse, size: 16), text: 'محاكي القواعد والأزمنة'),
|
||||
Tab(icon: Icon(CupertinoIcons.waveform, size: 16), text: 'استوديو الصوتيات والنطق'),
|
||||
Tab(icon: Icon(CupertinoIcons.rectangle_stack_fill, size: 16), text: 'بنك المفردات التفاعلي'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Tab Content
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildGrammarSimulatorView(),
|
||||
_buildPhoneticsStudioView(),
|
||||
_buildVocabularyBankView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAB 1: GRAMMAR & TENSES SIMULATOR
|
||||
// ============================================================================
|
||||
Widget _buildGrammarSimulatorView() {
|
||||
final activeTopic = _grammarTopics[_selectedGrammarTopicIndex];
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Left: Topics Menu
|
||||
Container(
|
||||
width: 260,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
border: Border(left: BorderSide(color: AppColors.darkCardBorder)),
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
itemCount: _grammarTopics.length,
|
||||
itemBuilder: (context, index) {
|
||||
final topic = _grammarTopics[index];
|
||||
final isSelected = _selectedGrammarTopicIndex == index;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.saqelCyan.withAlpha(20) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
topic['title'] as String,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.saqelCyan : Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
topic['badge'] as String,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 10),
|
||||
),
|
||||
onTap: () => setState(() => _selectedGrammarTopicIndex = index),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Right: Interactive Grammar Canvas
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
activeTopic['badge'] as String,
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.speaker_2, color: AppColors.saqelCyan, size: 20),
|
||||
onPressed: () => _speakWord(activeTopic['concept'] as String),
|
||||
tooltip: 'استمع للشرح باللغة الإنجليزية',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
activeTopic['concept'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.6),
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 28),
|
||||
|
||||
// Grammar Formulas
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black45,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(50)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('صيغة الإثبات الأولى:', style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
activeTopic['formula_present_perfect'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontFamily: 'Courier', fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black45,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.guardianAmber.withAlpha(50)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('صيغة المقارنة المقابلة:', style: TextStyle(color: AppColors.guardianAmber, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
activeTopic['formula_past_simple'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontFamily: 'Courier', fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
const Text(
|
||||
'أمثلة تفاعلية ومقارنة دلالية (Action Pack 10 Context):',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Comparison Cards
|
||||
...((activeTopic['pairs'] as List).map((pair) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text('المثال الأول', style: TextStyle(color: AppColors.saqelCyan, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.volume_up, color: AppColors.saqelCyan, size: 16),
|
||||
onPressed: () => _speakWord(pair['present'] as String),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
pair['present'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w700),
|
||||
),
|
||||
Text(
|
||||
pair['present_ar'] as String,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 18),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.guardianAmber.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text('المثال المقابل', style: TextStyle(color: AppColors.guardianAmber, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.volume_up, color: AppColors.guardianAmber, size: 16),
|
||||
onPressed: () => _speakWord(pair['past'] as String),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
pair['past'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w700),
|
||||
),
|
||||
Text(
|
||||
pair['past_ar'] as String,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
})),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAB 2: PHONETICS & PRONUNCIATION STUDIO (IPA & 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(
|
||||
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(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)),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAB 3: VOCABULARY FLASHCARDS BANK
|
||||
// ============================================================================
|
||||
Widget _buildVocabularyBankView() {
|
||||
final activeUnit = _vocabUnits[_selectedVocabUnitIndex];
|
||||
final words = activeUnit['words'] as List;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Unit Filter Bar
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
color: AppColors.darkSurface,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(_vocabUnits.length, (i) {
|
||||
final isSelected = _selectedVocabUnitIndex == i;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(
|
||||
_vocabUnits[i]['unit'] as String,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColors.saqelCyan,
|
||||
backgroundColor: AppColors.darkCard,
|
||||
onSelected: (_) => setState(() => _selectedVocabUnitIndex = i),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Flashcards Grid
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.all(20),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
childAspectRatio: 1.35,
|
||||
),
|
||||
itemCount: words.length,
|
||||
itemBuilder: (context, index) {
|
||||
final w = words[index];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: AppColors.darkCardBorder),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 10, offset: Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
w['word'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
w['ipa'] as String,
|
||||
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontFamily: 'Courier'),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.volume_up, color: AppColors.saqelCyan, size: 18),
|
||||
onPressed: () => _speakWord(w['word'] as String),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
w['ar'] as String,
|
||||
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
w['def'] as String,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 11, height: 1.4),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Divider(color: AppColors.darkCardBorder, height: 12),
|
||||
Text(
|
||||
'Collocations: ${w['collocations']}',
|
||||
style: const TextStyle(color: Color(0xFF34C759), fontSize: 10, fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE - GEOGEBRA & MATH SYSTEMS INTERACTIVE LAB (120 FPS)
|
||||
/// ==============================================================================
|
||||
///
|
||||
/// معمل برمجية جيوجبرا التفاعلي لحل أنظمة المعادلات بيانياً وجبرياً (الصف العاشر الأساسي):
|
||||
/// 1. رسم المنحنيات الديكارتية (خطوط مستقيمة، قطع مكافئ، دوائر) بدقة متناهية.
|
||||
/// 2. كشف نقاط التقاطع الآنية وحساب المميز (Discriminant: Δ = b² - 4ac).
|
||||
/// 3. محاكاة نشاط الكتاب الوزاري (صفحة 16 - 17): x² + y² = 13 مع x² - y = 7.
|
||||
/// 4. شروحات جبرية سقراطية خطوة بخطوة لكل تدريب.
|
||||
class MathInteractiveLabView extends StatefulWidget {
|
||||
final String? initialPreset;
|
||||
|
||||
const MathInteractiveLabView({
|
||||
super.key,
|
||||
this.initialPreset,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MathInteractiveLabView> createState() => _MathInteractiveLabViewState();
|
||||
}
|
||||
|
||||
class _MathInteractiveLabViewState extends State<MathInteractiveLabView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
// Grapher Parameters
|
||||
// Curve 1: Circle (x² + y² = r²) or Parabola (y = a1*x² + c1)
|
||||
int _curve1Type = 0; // 0 = Circle (x² + y² = r²), 1 = Parabola (y = a*x² + c)
|
||||
double _radiusSquared = 13.0;
|
||||
double _a1 = 1.0;
|
||||
double _c1 = -7.0;
|
||||
|
||||
// Curve 2: Parabola (y = x² - 7) or Line (y = m*x + b)
|
||||
int _curve2Type = 1; // 0 = Line (y = m*x + b), 1 = Parabola (y = x² + c2)
|
||||
double _m = 1.0;
|
||||
double _b = 1.0;
|
||||
double _c2 = -7.0;
|
||||
|
||||
// Zoom scale
|
||||
double _scale = 22.0;
|
||||
|
||||
// Selected Textbook Exercise Index (0 to 5)
|
||||
int _selectedExerciseIndex = 0;
|
||||
|
||||
final List<Map<String, dynamic>> _textbookExercises = [
|
||||
{
|
||||
'title': 'نشاط معمل جيوجبرا (صفحة 16)',
|
||||
'system': 'x² + y² = 13 \n x² - y = 7',
|
||||
'eq1': 'x² + y² = 13',
|
||||
'eq2': 'y = x² - 7',
|
||||
'r2': 13.0,
|
||||
'c2': -7.0,
|
||||
'type1': 0, // circle
|
||||
'type2': 1, // parabola
|
||||
'delta': 144.0,
|
||||
'points': [
|
||||
const Offset(3.0, 2.0),
|
||||
const Offset(-3.0, 2.0),
|
||||
const Offset(2.0, -3.0),
|
||||
const Offset(-2.0, -3.0),
|
||||
],
|
||||
'explanation':
|
||||
'بالتعويض عن x² = y + 7 في معادلة الدائرة: (y + 7) + y² = 13 ⟹ y² + y - 6 = 0 ⟹ (y + 3)(y - 2) = 0. ينتج قيمتان لـ y ومنهما 4 نقاط تقاطع.',
|
||||
},
|
||||
{
|
||||
'title': 'تدريب 1 (صفحة 17 - مميز سالب)',
|
||||
'system': 'x² + y² = 9 \n y = 2x + 10',
|
||||
'eq1': 'x² + y² = 9',
|
||||
'eq2': 'y = 2x + 10',
|
||||
'r2': 9.0,
|
||||
'm': 2.0,
|
||||
'b': 10.0,
|
||||
'type1': 0, // circle
|
||||
'type2': 0, // line
|
||||
'delta': -144.0,
|
||||
'points': <Offset>[],
|
||||
'explanation':
|
||||
'المميز Δ = -144 < 0؛ المستقيم يقع تماماً خارج الدائرة ولا يقطعها في أي نقطة حقيقية. مجموعة الحل = ∅.',
|
||||
},
|
||||
{
|
||||
'title': 'تدريب 2 (صفحة 17 - تقاطع مزدوج)',
|
||||
'system': 'x² + y² = 20 \n x = 4',
|
||||
'eq1': 'x² + y² = 20',
|
||||
'eq2': 'x = 4 (y² = 4)',
|
||||
'r2': 20.0,
|
||||
'c2': 0.0,
|
||||
'type1': 0,
|
||||
'type2': 0,
|
||||
'delta': 16.0,
|
||||
'points': [const Offset(4.0, 2.0), const Offset(4.0, -2.0)],
|
||||
'explanation':
|
||||
'عند تعويض x = 4 في معادلة الدائرة: 16 + y² = 20 ⟹ y² = 4 ⟹ y = ±2. إذن نقطتا التقاطع هما: (4, 2) و (4, -2).',
|
||||
},
|
||||
{
|
||||
'title': 'تدريب 3 (صفحة 17 - نقطة تقاطع تقريبية)',
|
||||
'system': 'y = x² - 1 \n y = 0.5x + 7',
|
||||
'eq1': 'y = x² - 1',
|
||||
'eq2': 'y = 0.5x + 7',
|
||||
'a1': 1.0,
|
||||
'c1': -1.0,
|
||||
'm': 0.5,
|
||||
'b': 7.0,
|
||||
'type1': 1, // parabola
|
||||
'type2': 0, // line
|
||||
'delta': 32.25,
|
||||
'points': [const Offset(3.09, 8.54), const Offset(-2.59, 5.70)],
|
||||
'explanation':
|
||||
'بمساواة المعادلتين: x² - 0.5x - 8 = 0. المميز موجب والمستقيم قاطع للقطع المكافئ في نقطتين.',
|
||||
},
|
||||
{
|
||||
'title': 'تدريب 4 (صفحة 17 - لا تقاطع)',
|
||||
'system': 'x² + y² = 4 \n y = -x² - 5',
|
||||
'eq1': 'x² + y² = 4',
|
||||
'eq2': 'y = -x² - 5',
|
||||
'r2': 4.0,
|
||||
'c2': -5.0,
|
||||
'type1': 0,
|
||||
'type2': 1,
|
||||
'delta': -295.0,
|
||||
'points': <Offset>[],
|
||||
'explanation':
|
||||
'المميز Δ = -295 < 0؛ المنحنيان متباعدان هندسياً ولا يتقاطعان. مجموعة الحل خالية ∅.',
|
||||
},
|
||||
{
|
||||
'title': 'تدريب 5 (صفحة 17 - مماسان دقيقان)',
|
||||
'system': 'x² + y² = 1 \n y = 6x',
|
||||
'eq1': 'x² + y² = 1',
|
||||
'eq2': 'y = 6x',
|
||||
'r2': 1.0,
|
||||
'm': 6.0,
|
||||
'b': 0.0,
|
||||
'type1': 0,
|
||||
'type2': 0,
|
||||
'delta': 148.0,
|
||||
'points': [
|
||||
const Offset(0.493, 2.959),
|
||||
const Offset(-0.493, -2.959),
|
||||
],
|
||||
'explanation':
|
||||
'المستقيم يمر بنقطة الأصل ويقطع الدائرة في نقطتين متناظرتين حول المركز: (±3/√37, ±18/√37).',
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_applyTextbookPreset(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _applyTextbookPreset(int index) {
|
||||
final ex = _textbookExercises[index];
|
||||
setState(() {
|
||||
_selectedExerciseIndex = index;
|
||||
_curve1Type = ex['type1'] as int;
|
||||
_curve2Type = ex['type2'] as int;
|
||||
if (ex.containsKey('r2')) _radiusSquared = (ex['r2'] as num).toDouble();
|
||||
if (ex.containsKey('c1')) _c1 = (ex['c1'] as num).toDouble();
|
||||
if (ex.containsKey('c2')) _c2 = (ex['c2'] as num).toDouble();
|
||||
if (ex.containsKey('m')) _m = (ex['m'] as num).toDouble();
|
||||
if (ex.containsKey('b')) _b = (ex['b'] as num).toDouble();
|
||||
if (ex.containsKey('a1')) _a1 = (ex['a1'] as num).toDouble();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Column(
|
||||
children: [
|
||||
// Sub-navigation bar
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
border: Border(bottom: BorderSide(color: AppColors.darkCardBorder)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.function, color: AppColors.saqelCyan, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'معمل برمجية جيوجبرا (GeoGebra Lab) — الصف العاشر',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
|
||||
),
|
||||
child: const Text(
|
||||
'محاكي 120 FPS',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Main Tab selector
|
||||
Container(
|
||||
color: AppColors.darkSurface,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: AppColors.saqelCyan,
|
||||
indicatorWeight: 3,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: AppColors.textSecondaryDark,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
tabs: const [
|
||||
Tab(icon: Icon(CupertinoIcons.graph_circle_fill, size: 16), text: 'الراسم البياني التفاعلي'),
|
||||
Tab(icon: Icon(CupertinoIcons.book_fill, size: 16), text: 'تطبيقات الكتاب (صفحة 16 - 17)'),
|
||||
Tab(icon: Icon(CupertinoIcons.check_mark_circled_solid, size: 16), text: 'خطوات الحل والمميز'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Tab Content
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildInteractiveGrapherView(),
|
||||
_buildTextbookActivitiesView(),
|
||||
_buildStepByStepSolverView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAB 1: INTERACTIVE GRAPHING CANVAS
|
||||
// ============================================================================
|
||||
Widget _buildInteractiveGrapherView() {
|
||||
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)),
|
||||
],
|
||||
),
|
||||
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: 'إعادة ضبط المحاور',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 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),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
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: 24),
|
||||
|
||||
// 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: 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
|
||||
const Text(
|
||||
'نماذج تدريبات الكتاب الوزاري:',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: List.generate(_textbookExercises.length, (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),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAB 2: TEXTBOOK ACTIVITIES & GEOGEBRA SCRIPT (PAGES 16 - 17)
|
||||
// ============================================================================
|
||||
Widget _buildTextbookActivitiesView() {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(20),
|
||||
itemCount: _textbookExercises.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _textbookExercises[index];
|
||||
final points = item['points'] as List<Offset>;
|
||||
final isSelected = _selectedExerciseIndex == index;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.saqelCyan.withAlpha(15) : AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.saqelCyan : AppColors.darkCardBorder,
|
||||
width: isSelected ? 1.5 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.saqelCyan : AppColors.darkCard,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
item['title'] as String,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : AppColors.saqelCyan,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.play_arrow_solid, size: 14),
|
||||
label: const Text('تشغيل في الراسم'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.saqelCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
textStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
onPressed: () {
|
||||
_applyTextbookPreset(index);
|
||||
_tabController.animateTo(0);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black38,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white10),
|
||||
),
|
||||
child: Text(
|
||||
'نظام المعادلات:\n${item['system']}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Courier',
|
||||
color: AppColors.saqelCyan,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.smallcircle_fill_circle, color: Color(0xFFFF2D55), size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
points.isEmpty
|
||||
? 'مجموعة الحل: ∅ (المميز Δ = ${item['delta']} < 0)'
|
||||
: 'نقاط التقاطع: ${points.map((p) => "(${p.dx.toStringAsFixed(1)}, ${p.dy.toStringAsFixed(1)})").join(" , ")}',
|
||||
style: TextStyle(
|
||||
color: points.isEmpty ? Colors.white70 : const Color(0xFFFF2D55),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
item['explanation'] as String,
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12, height: 1.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAB 3: STEP-BY-STEP ALGEBRAIC SOLVER & SOCRATIC DISCRIMINANT RADAR
|
||||
// ============================================================================
|
||||
Widget _buildStepByStepSolverView() {
|
||||
final active = _textbookExercises[_selectedExerciseIndex];
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.wand_rays_inverse, color: AppColors.saqelCyan, size: 22),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'خوارزمية الحل الجبري المنهجي — ${active['title']}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'الخطوة 1: جعل أحد المتغيرين موضوعاً للقانون',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'من المعادلة الثانية: نجعل المتغير الخطي أو التربيعي في طرف مستقل:\n${active['eq2']}',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'الخطوة 2: التعويض في المعادلة الأولى',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'نعوض التعبير الجبري في معادلة المنحنى الأول: ${active['eq1']} للحصول على معادلة بمتغير واحد.',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'الخطوة 3: حساب المميز الجنائي (Discriminant: Δ = b² - 4ac)',
|
||||
style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black45,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.guardianAmber.withAlpha(60)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'قيمة المميز: Δ = ${active['delta']}',
|
||||
style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
(active['delta'] as double) > 0
|
||||
? 'يوجد حلان حقيقيان (قاطع)'
|
||||
: (active['delta'] as double) == 0
|
||||
? 'يوجد حل حقيقي وحيد (مماس)'
|
||||
: 'المميز سالب: لا يوجد تقاطع (∅)',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'الخطوة 4: التحليل واستخراج نقاط التقاطع النهائية',
|
||||
style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.w700, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
active['explanation'] as String,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 120 FPS CARTESIAN CANVAS PAINTER
|
||||
// ==============================================================================
|
||||
class _CartesianCanvasPainter extends CustomPainter {
|
||||
final int curve1Type;
|
||||
final double radiusSquared;
|
||||
final double a1;
|
||||
final double c1;
|
||||
final int curve2Type;
|
||||
final double m;
|
||||
final double b;
|
||||
final double c2;
|
||||
final double scale;
|
||||
final List<Offset> intersectionPoints;
|
||||
|
||||
_CartesianCanvasPainter({
|
||||
required this.curve1Type,
|
||||
required this.radiusSquared,
|
||||
required this.a1,
|
||||
required this.c1,
|
||||
required this.curve2Type,
|
||||
required this.m,
|
||||
required this.b,
|
||||
required this.c2,
|
||||
required this.scale,
|
||||
required this.intersectionPoints,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final cx = size.width / 2;
|
||||
final cy = size.height / 2;
|
||||
|
||||
// 1. Draw Grid Lines
|
||||
final gridPaint = Paint()
|
||||
..color = const Color(0xFF1B263B)
|
||||
..strokeWidth = 0.8;
|
||||
|
||||
for (double x = cx % scale; x < size.width; x += scale) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
|
||||
}
|
||||
for (double y = cy % scale; y < size.height; y += scale) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
|
||||
}
|
||||
|
||||
// 2. Draw Main Axes (X and Y)
|
||||
final axisPaint = Paint()
|
||||
..color = Colors.white38
|
||||
..strokeWidth = 1.5;
|
||||
|
||||
canvas.drawLine(Offset(0, cy), Offset(size.width, cy), axisPaint);
|
||||
canvas.drawLine(Offset(cx, 0), Offset(cx, size.height), axisPaint);
|
||||
|
||||
// 3. Draw Curve 1 (Cyan)
|
||||
final curve1Paint = Paint()
|
||||
..color = AppColors.saqelCyan
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.5;
|
||||
|
||||
if (curve1Type == 0) {
|
||||
// Circle: x² + y² = r²
|
||||
final rPixel = math.sqrt(radiusSquared) * scale;
|
||||
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 mathY = a1 * mathX * mathX + c1;
|
||||
final py = cy - mathY * scale;
|
||||
if (!started) {
|
||||
path.moveTo(px, py);
|
||||
started = true;
|
||||
} else {
|
||||
path.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, curve1Paint);
|
||||
}
|
||||
|
||||
// 4. Draw Curve 2 (Amber)
|
||||
final curve2Paint = Paint()
|
||||
..color = AppColors.guardianAmber
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.5;
|
||||
|
||||
if (curve2Type == 0) {
|
||||
// Line: y = m*x + b
|
||||
final x1 = (0 - cx) / scale;
|
||||
final y1 = m * x1 + b;
|
||||
final py1 = cy - y1 * scale;
|
||||
|
||||
final x2 = (size.width - cx) / scale;
|
||||
final y2 = m * x2 + b;
|
||||
final py2 = cy - y2 * scale;
|
||||
|
||||
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 mathY = mathX * mathX + c2;
|
||||
final py = cy - mathY * scale;
|
||||
if (!started) {
|
||||
path.moveTo(px, py);
|
||||
started = true;
|
||||
} else {
|
||||
path.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, curve2Paint);
|
||||
}
|
||||
|
||||
// 5. Draw Intersection Points (Red Glow)
|
||||
final ptGlow = Paint()
|
||||
..color = const Color(0xFFFF2D55).withAlpha(100)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final ptPaint = Paint()
|
||||
..color = const Color(0xFFFF2D55)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
for (var pt in intersectionPoints) {
|
||||
final px = cx + pt.dx * scale;
|
||||
final py = cy - pt.dy * scale;
|
||||
|
||||
canvas.drawCircle(Offset(px, py), 9.0, ptGlow);
|
||||
canvas.drawCircle(Offset(px, py), 5.0, ptPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _CartesianCanvasPainter oldDelegate) {
|
||||
return oldDelegate.curve1Type != curve1Type ||
|
||||
oldDelegate.radiusSquared != radiusSquared ||
|
||||
oldDelegate.a1 != a1 ||
|
||||
oldDelegate.c1 != c1 ||
|
||||
oldDelegate.curve2Type != curve2Type ||
|
||||
oldDelegate.m != m ||
|
||||
oldDelegate.b != b ||
|
||||
oldDelegate.c2 != c2 ||
|
||||
oldDelegate.scale != scale ||
|
||||
oldDelegate.intersectionPoints != intersectionPoints;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import 'physics_interactive_lab_view.dart';
|
||||
import 'arabic_interactive_lab_view.dart';
|
||||
import 'islamic_interactive_lab_view.dart';
|
||||
import 'history_interactive_timeline_view.dart';
|
||||
import 'math_interactive_lab_view.dart';
|
||||
import 'english_interactive_lab_view.dart';
|
||||
import '../../../data/models/exam_model.dart';
|
||||
import '../../../data/repositories/app_repositories.dart';
|
||||
import '../../../core/services/local_document_storage_service.dart';
|
||||
@@ -44,6 +46,15 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
||||
late TabController _tabController;
|
||||
late Future<List<ExamModel>> _examsFuture;
|
||||
|
||||
bool get _isMathSubject =>
|
||||
widget.subject.id.contains('math') ||
|
||||
widget.subject.title.contains('رياضيات');
|
||||
|
||||
bool get _isEnglishSubject =>
|
||||
widget.subject.id.contains('eng') ||
|
||||
widget.subject.title.contains('إنجليز') ||
|
||||
widget.subject.title.contains('لغة إنجليزية');
|
||||
|
||||
bool get _isScienceSubject =>
|
||||
widget.subject.id.contains('physic') ||
|
||||
widget.subject.title.contains('فيزياء') ||
|
||||
@@ -72,13 +83,25 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
||||
widget.subject.title.contains('دراسات');
|
||||
|
||||
bool get _hasLab =>
|
||||
_isMathSubject ||
|
||||
_isEnglishSubject ||
|
||||
_isScienceSubject ||
|
||||
_isArabicSubject ||
|
||||
_isIslamicSubject ||
|
||||
_isHistorySubject;
|
||||
|
||||
Tab get _dynamicLabTab {
|
||||
if (_isArabicSubject) {
|
||||
if (_isMathSubject) {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.function, size: 18),
|
||||
text: 'معمل جيوجبرا للرياضيات 📐',
|
||||
);
|
||||
} else if (_isEnglishSubject) {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.headphones, size: 18),
|
||||
text: 'مختبر الصوتيات والقواعد 🎧',
|
||||
);
|
||||
} else if (_isArabicSubject) {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.arrow_branch, size: 18),
|
||||
text: 'مختبر الإعراب والعروض 📜',
|
||||
@@ -102,7 +125,11 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
||||
}
|
||||
|
||||
Widget get _dynamicLabWidget {
|
||||
if (_isArabicSubject) {
|
||||
if (_isMathSubject) {
|
||||
return const MathInteractiveLabView();
|
||||
} else if (_isEnglishSubject) {
|
||||
return const EnglishInteractiveLabView();
|
||||
} else if (_isArabicSubject) {
|
||||
return const ArabicInteractiveLabView();
|
||||
} else if (_isIslamicSubject) {
|
||||
return const IslamicInteractiveLabView();
|
||||
|
||||
+177
-24
@@ -26,6 +26,8 @@ import '../../../logic/cubits/video_playback_cubit.dart';
|
||||
import '../../widgets/luxury_widgets.dart';
|
||||
import '../../widgets/socratic_dialog.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import '../curriculum/math_interactive_lab_view.dart';
|
||||
|
||||
/// مشغل الفيديو السقراطي الذكي ونقاط الفحص والإرجاع العلاجي
|
||||
class SocraticVideoPlayerScreen extends StatefulWidget {
|
||||
@@ -55,6 +57,8 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
int _lastSyncedPosition = -1;
|
||||
Timer? _controlsHideTimer;
|
||||
bool _showVideoControls = true;
|
||||
final FlutterTts _tts = FlutterTts();
|
||||
bool _isSpeakingChalkboard = false;
|
||||
|
||||
void _revealVideoControls() {
|
||||
if (!mounted) return;
|
||||
@@ -65,6 +69,21 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _speakConcept(String text) async {
|
||||
if (text.isEmpty) return;
|
||||
setState(() => _isSpeakingChalkboard = true);
|
||||
try {
|
||||
await _tts.setLanguage('ar');
|
||||
await _tts.setSpeechRate(0.48);
|
||||
_tts.setCompletionHandler(() {
|
||||
if (mounted) setState(() => _isSpeakingChalkboard = false);
|
||||
});
|
||||
await _tts.speak(text);
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _isSpeakingChalkboard = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -97,6 +116,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tts.stop();
|
||||
_playbackTicker?.cancel();
|
||||
_controlsHideTimer?.cancel();
|
||||
_watermarkController.dispose();
|
||||
@@ -319,30 +339,10 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
),
|
||||
),
|
||||
),
|
||||
// Center Lesson Title & Visuals
|
||||
if (_videoInitError != null)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.videocam_fill, color: AppColors.guardianAmber, size: 44),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_videoInitError!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'يمكنك دراسة نتاجات وملخص الدرس عبر تبويب الكتب والمذكرات',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Center Lesson Title & Visuals / Interactive Socratic Chalkboard
|
||||
if (_videoInitError != null || !_isVideoInitialized || state.playbackData.videoUrl.isEmpty)
|
||||
Positioned.fill(
|
||||
child: _buildInteractiveChalkboard(state),
|
||||
)
|
||||
else if (_showVideoControls) Center(
|
||||
child: Column(
|
||||
@@ -772,6 +772,159 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInteractiveChalkboard(VideoPlaybackReady state) {
|
||||
final cp = state.playbackData.checkpoints.isNotEmpty ? state.playbackData.checkpoints.first : null;
|
||||
final explanation = cp?.pedagogicalExplanation ??
|
||||
'الدرس الوزاري المعتمد: يركز على إتقان المفاهيم الرياضية والعلمية وحل المسائل التحليلية خطوة بخطوة.';
|
||||
final hint = cp?.hint ?? 'راجع خطوات الحل ونظم المتغيرات بدقة.';
|
||||
final isMath = (widget.subject?.id ?? '').contains('math') || widget.lesson.title.contains('معادل') || widget.lesson.title.contains('رياضيات');
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: Alignment(0.0, -0.2),
|
||||
radius: 1.2,
|
||||
colors: [Color(0xFF0F2537), Color(0xFF070D18)],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(90)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'لوحة الشرح السقراطية التفاعلية 💡',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
widget.lesson.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(120),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
explanation,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'💡 إرشاد ذهني: $hint',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: AppColors.guardianAmber, fontSize: 11, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(_isSpeakingChalkboard ? CupertinoIcons.waveform : CupertinoIcons.volume_up, size: 16),
|
||||
label: Text(_isSpeakingChalkboard ? 'جاري الاستماع...' : 'استمع للشرح الصوتي 🎧'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.saqelCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () => _speakConcept('$explanation. $hint'),
|
||||
),
|
||||
if (cp != null)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.question_circle_fill, size: 16),
|
||||
label: const Text('فحص الفهم السقراطي ⚡'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.appleBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (modalContext) => SocraticCheckpointModal(
|
||||
checkpoint: cp,
|
||||
onOptionSelected: (opt) {
|
||||
Navigator.of(modalContext).pop();
|
||||
context.read<VideoPlaybackCubit>().submitCheckpointAnswer(opt);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isMath)
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.function, size: 16, color: AppColors.saqelCyan),
|
||||
label: const Text('معمل جيوجبرا للتحقق بيانياً 📐', style: TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.w700)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: AppColors.saqelCyan),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: AppColors.darkBackground,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (ctx) => const SizedBox(
|
||||
height: 600,
|
||||
child: MathInteractiveLabView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'فيديو المعلم قيد المزامنة الرقمية والتجهيز؛ تم تفعيل لوحة الشرح التفاعلية لضمان التعلم الفوري المستمر 🚀',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridPatternPainter extends CustomPainter {
|
||||
|
||||
Reference in New Issue
Block a user