Update Saqel Platform: 2026-09-12 16:23:39
This commit is contained in:
@@ -52,7 +52,7 @@ class CurriculumQuestionBank {
|
||||
if (s.contains('chem') || s.contains('كيمياء')) return 'chemistry_10';
|
||||
if (s.contains('bio') || s.contains('أحياء') || s.contains('حياتية')) return 'biology_10';
|
||||
if (s.contains('earth') || s.contains('أرض') || s.contains('بيئة') || s.contains('geology')) return 'earth_sciences_10';
|
||||
if (s.contains('hist') || s.contains('تاريخ')) return 'history_10';
|
||||
if (s.contains('hist') || s.contains('تاريخ') || s.contains('أردن') || s.contains('اردن')) return 'history_10';
|
||||
if (s.contains('geo') || s.contains('جغرافيا')) return 'geography_10';
|
||||
if (s.contains('eng') || s.contains('إنجليز') || s.contains('انجليز')) return 'english_10';
|
||||
if (s.contains('civic') || s.contains('وطنية') || s.contains('مدنية')) return 'civics_10';
|
||||
@@ -383,6 +383,52 @@ class CurriculumQuestionBank {
|
||||
} else {
|
||||
items = _buildGenericLessonHomework(subjectTitle: 'العربية لغتي', lessonTitle: lessonTitle);
|
||||
}
|
||||
} else if (key == 'history_10') {
|
||||
if (lTitle.contains('فارس') || lTitle.contains('أخمين') || lId.contains('01')) {
|
||||
items = [
|
||||
const LessonHomeworkItem(
|
||||
id: 401,
|
||||
questionText: 'تأسست الدولة الأخمينية (الإمبراطورية الفارسية الأولى) على يد الملك كورش، وكانت عاصمتها الشهيرة:',
|
||||
options: [
|
||||
'برسبوليس (Persepolis)',
|
||||
'بابل في بلاد الرافدين',
|
||||
'طيبة في مصر القديمة',
|
||||
'أثينا في بلاد الإغريق',
|
||||
],
|
||||
correctIndex: 0,
|
||||
explanation: 'اتخذ الملك كورش والملوك الأخمينيون من مدينة برسبوليس عاصمة كبرى للإمبراطورية، واشتهرت بعمارتها وقصورها المهيبة.',
|
||||
ruleTakeaway: 'برسبوليس هي العاصمة التاريخية للأخمينيين.',
|
||||
),
|
||||
const LessonHomeworkItem(
|
||||
id: 402,
|
||||
questionText: 'ما الدلالة التاريخية لاكتشاف حفر خزن الحبوب وقطع الفخار اليونانية في موقع تل الفخار (إربد) ومغارة الوردة (عجلون) خلال العصر الفارسي؟',
|
||||
options: [
|
||||
'حدوث مجاعة شديدة في المنطقة',
|
||||
'انتعاش الأحوال الاقتصادية والتبادل التجاري مع اليونان وفينيقيا',
|
||||
'انعدام الاستقرار السكاني في شمال الأردن',
|
||||
'اقتصار التجارة على الملح فقط',
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation: 'تشير حفر خزن المحاصيل والقطع المستوردة الثمينة إلى وجود مجتمع متمدن واستقرار اقتصادي وتبادل تجاري نشط عبر موانئ البحر المتوسط.',
|
||||
ruleTakeaway: 'تل الفخار ومغارة الوردة شواهد أثرية على ازدهار الأردن في العصر الفارسي.',
|
||||
),
|
||||
const LessonHomeworkItem(
|
||||
id: 403,
|
||||
questionText: 'انتهت آخر مراحل الإمبراطورية الفارسية (الدولة الساسانية) بعد معركة نهاوند الفاصلة سنة 642م ومقتل آخر ملوكها وهو:',
|
||||
options: [
|
||||
'يزدجرد الثالث',
|
||||
'قمبيز الثاني',
|
||||
'كورش الكبير',
|
||||
'سلوقس الأول',
|
||||
],
|
||||
correctIndex: 0,
|
||||
explanation: 'معركة نهاوند (فتح الفتوح) أسقطت القوة العسكرية للساسانيين، وانتهت الدولة الفارسية بمقتل يزدجرد الثالث عام 651م.',
|
||||
ruleTakeaway: 'نهاوند أنهت الوجود السياسي للإمبراطورية الفارسية الساسانية.',
|
||||
),
|
||||
];
|
||||
} else {
|
||||
items = _buildGenericLessonHomework(subjectTitle: 'التاريخ', lessonTitle: lessonTitle);
|
||||
}
|
||||
} else {
|
||||
items = _buildGenericLessonHomework(subjectTitle: subjectId, lessonTitle: lessonTitle);
|
||||
}
|
||||
@@ -1162,17 +1208,77 @@ class CurriculumQuestionBank {
|
||||
return [
|
||||
const QuestionModel(
|
||||
id: 7101,
|
||||
questionText: 'المعلم الأثري البارز في الأردن الذي شيده الأنباط عاصمةً لحضارتهم التجارية في جنوب بلاد الشام هو:',
|
||||
questionText: 'تأسست الإمبراطورية الفارسية الأولى (الدولة الأخمينية) عام 560 ق.م على يد الملك:',
|
||||
questionType: 'multiple_choice',
|
||||
points: 10,
|
||||
topicTag: 'تاريخ الأردن — الحضارة النبطية والتجارة القديمة',
|
||||
explanationText: 'البترا هي العاصمة النبطية المنحوتة في الصخر الوردي، سيطر الأنباط من خلالها على طرق القوافل التجارية بين اليمن والشام ومصر.',
|
||||
aiHint: 'المدينة الوردية المنحوتة في الصخر إحدى عجائب الدنيا.',
|
||||
topicTag: 'تاريخ الصف العاشر — الدولة الأخمينية وتأسيس الإمبراطورية الفارسية',
|
||||
explanationText: 'تأسست الإمبراطورية الفارسية على يد الملك كورش الذي حكم بين (560 ق.م - 529 ق.م)، وجعل عاصمتها مدينة برسبوليس.',
|
||||
aiHint: 'الملك الذي أسس الدولة الأخمينية ووسع نفوذها حتى وادي السند.',
|
||||
options: [
|
||||
QuestionOptionModel(id: 81, optionText: 'جرش وأعمدتها الرومانية', isCorrect: false),
|
||||
QuestionOptionModel(id: 82, optionText: 'البترا المنحوتة في الصخر الوردي', isCorrect: true),
|
||||
QuestionOptionModel(id: 83, optionText: 'قلعة عجلون الأيوبية', isCorrect: false),
|
||||
QuestionOptionModel(id: 84, optionText: 'أم قيس المطلة على طبريا', isCorrect: false),
|
||||
QuestionOptionModel(id: 81, optionText: 'داريوس الأكبر', isCorrect: false),
|
||||
QuestionOptionModel(id: 82, optionText: 'كورش (Cyrus)', isCorrect: true),
|
||||
QuestionOptionModel(id: 83, optionText: 'قمبيز الثاني', isCorrect: false),
|
||||
QuestionOptionModel(id: 84, optionText: 'أرساكيس الأول', isCorrect: false),
|
||||
],
|
||||
),
|
||||
const QuestionModel(
|
||||
id: 7102,
|
||||
questionText: 'تمكن ملك الدولة الأخمينية الذي خلف كورش من غزو مصر وضمها إلى الإمبراطورية الفارسية، وهو الملك:',
|
||||
questionType: 'multiple_choice',
|
||||
points: 10,
|
||||
topicTag: 'تاريخ الصف العاشر — ملوك الدولة الأخمينية والامتداد الجغرافي',
|
||||
explanationText: 'الملك قمبيز الثاني هو أشهر ملوك الدولة الأخمينية الذي تمكن من غزو مصر وضمها إلى مملكته الفارسية.',
|
||||
aiHint: 'ابن الملك كورش وخليفته في الحكم الذي توجه بحملاته غرباً نحو وادي النيل.',
|
||||
options: [
|
||||
QuestionOptionModel(id: 85, optionText: 'قمبيز الثاني', isCorrect: true),
|
||||
QuestionOptionModel(id: 86, optionText: 'سلوقس الأول', isCorrect: false),
|
||||
QuestionOptionModel(id: 87, optionText: 'الإسكندر المقدوني', isCorrect: false),
|
||||
QuestionOptionModel(id: 88, optionText: 'ميتراداتس الأول', isCorrect: false),
|
||||
],
|
||||
),
|
||||
const QuestionModel(
|
||||
id: 7103,
|
||||
questionText: 'خضع الأردن للنفوذ الفارسي عام 539 ق.م بعد هزيمة البابليين. من أهم المواقع الأثرية في الأردن التي عُثر فيها على شواهد وحفر لخزن الحبوب والمحاصيل من تلك الفترة:',
|
||||
questionType: 'multiple_choice',
|
||||
points: 10,
|
||||
topicTag: 'تاريخ الصف العاشر — الأردن في العصر الفارسي والشواهد الأثرية',
|
||||
explanationText: 'عثر على دلائل الاستقرار وحفر خزن الحبوب وقطع مستوردة من اليونان وفينيقيا في موقعي تل الفخار (شمال إربد) ومغارة الوردة (عجلون).',
|
||||
aiHint: 'موقعان بارزان أحدهما تل أثري شمال إربد والآخر مغارة في جبال عجلون.',
|
||||
options: [
|
||||
QuestionOptionModel(id: 89, optionText: 'تل الفخار (إربد) ومغارة الوردة (عجلون)', isCorrect: true),
|
||||
QuestionOptionModel(id: 90, optionText: 'البترا ووادي رم', isCorrect: false),
|
||||
QuestionOptionModel(id: 91, optionText: 'أم قيس وجرش الرومانية', isCorrect: false),
|
||||
QuestionOptionModel(id: 92, optionText: 'قلاع الشوبك والكرك الأيوبية', isCorrect: false),
|
||||
],
|
||||
),
|
||||
const QuestionModel(
|
||||
id: 7104,
|
||||
questionText: 'انتهت الدولة الأخمينية على يد القائد الإغريقي الإسكندر المقدوني، وأصبح الحكم في بلاد فارس بعد وفاته بيد:',
|
||||
questionType: 'multiple_choice',
|
||||
points: 10,
|
||||
topicTag: 'تاريخ الصف العاشر — الغزو اليوناني والدولة السلوقية',
|
||||
explanationText: 'بعد وفاة الإسكندر المقدوني قُسمت إمبراطوريته وأصبحت بلاد فارس وغرب آسيا تحت حكم الدولة السلوقية التي أسسها سلوقس الأول.',
|
||||
aiHint: 'الدولة الهلنستية المنسوبة لمؤسسها سلوقس الأول.',
|
||||
options: [
|
||||
QuestionOptionModel(id: 93, optionText: 'السلوقيين بقيادة سلوقس الأول', isCorrect: true),
|
||||
QuestionOptionModel(id: 94, optionText: 'الفراعنة في مصر', isCorrect: false),
|
||||
QuestionOptionModel(id: 95, optionText: 'الأنباط في البترا', isCorrect: false),
|
||||
QuestionOptionModel(id: 96, optionText: 'البيزنطيين في القسطنطينية', isCorrect: false),
|
||||
],
|
||||
),
|
||||
const QuestionModel(
|
||||
id: 7105,
|
||||
questionText: 'المرحلة التاريخية الرابعة والأخيرة للإمبراطورية الفارسية التي انتهت بعد معركة نهاوند عام 642م وسقوط يزدجرد الثالث هي:',
|
||||
questionType: 'multiple_choice',
|
||||
points: 10,
|
||||
topicTag: 'تاريخ الصف العاشر — الدولة الساسانية ومعركة نهاوند',
|
||||
explanationText: 'الدولة الساسانية (224م - 651م) هي المرحلة الأخيرة، وانتهت بانتصار المسلمين الحاسم في معركة نهاوند عام 642م ومقتل يزدجرد الثالث عام 651م.',
|
||||
aiHint: 'الدولة الفارسية الأخيرة التي اصطدمت بجيوش الفتح الإسلامي في القادسية ونهاوند.',
|
||||
options: [
|
||||
QuestionOptionModel(id: 97, optionText: 'الدولة الساسانية', isCorrect: true),
|
||||
QuestionOptionModel(id: 98, optionText: 'الدولة البارثية (الأشكانية)', isCorrect: false),
|
||||
QuestionOptionModel(id: 99, optionText: 'الدولة الأخمينية الأولى', isCorrect: false),
|
||||
QuestionOptionModel(id: 100, optionText: 'الدولة السلوقية', isCorrect: false),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
+235
-18
@@ -223,7 +223,7 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
|
||||
}
|
||||
currentTitle = trimmed.replaceAll(RegExp(r'^#{2,3}\s*'), '');
|
||||
} else if (trimmed.isNotEmpty) {
|
||||
currentLines.add(trimmed);
|
||||
currentLines.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,218 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Converts LaTeX / Chemical notation into crisp Unicode and clean labels
|
||||
String _cleanFormulas(String text) {
|
||||
var s = text;
|
||||
s = s.replaceAll(r'$$\text{CO}_2$$', 'CO₂');
|
||||
s = s.replaceAll(r'$$\text{H}_2\text{O}$$', 'H₂O');
|
||||
s = s.replaceAll(r'$$\text{O}_3$$', 'O₃');
|
||||
s = s.replaceAll(r'$$\text{O}_2$$', 'O₂');
|
||||
s = s.replaceAll(r'$$\text{N}_2$$', 'N₂');
|
||||
s = s.replaceAll(r'$$\text{CH}_4$$', 'CH₄');
|
||||
s = s.replaceAll(r'$$\text{SO}_2$$', 'SO₂');
|
||||
s = s.replaceAll(r'$$\text{NO}_2$$', 'NO₂');
|
||||
s = s.replaceAll(r'$$\text{CO}$$', 'CO');
|
||||
|
||||
// Generic pattern for $$\text{X}_n$$
|
||||
s = s.replaceAllMapped(RegExp(r'\$\$\\text\{([A-Za-z0-9]+)\}_([0-9]+)\$\$'), (m) {
|
||||
final element = m.group(1) ?? '';
|
||||
final sub = (m.group(2) ?? '').split('').map((d) {
|
||||
const subs = {'0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉'};
|
||||
return subs[d] ?? d;
|
||||
}).join('');
|
||||
return '$element$sub';
|
||||
});
|
||||
|
||||
// Strip generic $$ ... $$ or $ ... $ markers cleanly if text
|
||||
s = s.replaceAllMapped(RegExp(r'\$\$([^\$]+)\$\$'), (m) {
|
||||
var formula = m.group(1) ?? '';
|
||||
formula = formula.replaceAll(r'\text{', '').replaceAll('}', '');
|
||||
return formula.trim();
|
||||
});
|
||||
s = s.replaceAllMapped(RegExp(r'\$([^\$]+)\$'), (m) {
|
||||
var formula = m.group(1) ?? '';
|
||||
formula = formula.replaceAll(r'\text{', '').replaceAll('}', '');
|
||||
return formula.trim();
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
/// RichText builder parsing **bold** syntax into styled spans
|
||||
Widget _buildFormattedText(String rawText, {required double fontSize, Color? baseColor}) {
|
||||
final cleaned = _cleanFormulas(rawText);
|
||||
final spans = <TextSpan>[];
|
||||
final regex = RegExp(r'\*\*(.*?)\*\*');
|
||||
int lastMatchEnd = 0;
|
||||
|
||||
for (final match in regex.allMatches(cleaned)) {
|
||||
if (match.start > lastMatchEnd) {
|
||||
spans.add(TextSpan(
|
||||
text: cleaned.substring(lastMatchEnd, match.start),
|
||||
style: TextStyle(
|
||||
color: baseColor ?? Colors.white.withAlpha(225),
|
||||
fontSize: fontSize,
|
||||
height: 1.6,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
final boldText = match.group(1) ?? '';
|
||||
spans.add(TextSpan(
|
||||
text: boldText,
|
||||
style: TextStyle(
|
||||
color: baseColor ?? AppColors.saqelCyan,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: fontSize,
|
||||
height: 1.6,
|
||||
),
|
||||
));
|
||||
|
||||
lastMatchEnd = match.end;
|
||||
}
|
||||
|
||||
if (lastMatchEnd < cleaned.length) {
|
||||
spans.add(TextSpan(
|
||||
text: cleaned.substring(lastMatchEnd),
|
||||
style: TextStyle(
|
||||
color: baseColor ?? Colors.white.withAlpha(225),
|
||||
fontSize: fontSize,
|
||||
height: 1.6,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders individual lines with rich Cupertino styling: subheadings, bullets, callouts
|
||||
Widget _buildSectionLine(String line, double fontSize) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
// 1. Subheading (#### or ###)
|
||||
if (trimmed.startsWith('#### ') || trimmed.startsWith('### ')) {
|
||||
final title = trimmed.replaceAll(RegExp(r'^#{3,4}\s*'), '');
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 14, bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.appleBlue.withAlpha(25),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(50)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.saqelCyan,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_cleanFormulas(title),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: fontSize + 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Blockquote / Callout (> Note or check of understanding)
|
||||
if (trimmed.startsWith('>')) {
|
||||
final quoteText = trimmed.replaceAll(RegExp(r'^>\s*'), '');
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F1E36),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.guardianAmber.withAlpha(80)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.lightbulb_fill, color: AppColors.guardianAmber, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildFormattedText(quoteText, fontSize: fontSize, baseColor: Colors.white.withAlpha(235)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Sub-bullet (Indented with spaces + bullet)
|
||||
final isIndented = line.startsWith(' ') || line.startsWith('\t');
|
||||
if (isIndented && (trimmed.startsWith('-') || trimmed.startsWith('•') || trimmed.startsWith('*'))) {
|
||||
final itemText = trimmed.replaceFirst(RegExp(r'^[-•\*]\s*'), '');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 18, top: 4, bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 7),
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(160),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildFormattedText(itemText, fontSize: fontSize - 0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Primary Bullet (- or • or *)
|
||||
if (trimmed.startsWith('-') || trimmed.startsWith('•') || trimmed.startsWith('*')) {
|
||||
final itemText = trimmed.replaceFirst(RegExp(r'^[-•\*]\s*'), '');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 6, top: 6, bottom: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 7),
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.appleBlue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildFormattedText(itemText, fontSize: fontSize),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Standard paragraph line
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: _buildFormattedText(trimmed, fontSize: fontSize),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -618,31 +830,36 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
|
||||
else
|
||||
..._sections.map((section) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: LuxuryCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
section.title,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 14.5),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...section.lines.map((line) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
line,
|
||||
style: TextStyle(
|
||||
color: line.startsWith('-') || line.startsWith('•')
|
||||
? AppColors.textSecondaryDark
|
||||
: Colors.white.withAlpha(230),
|
||||
fontSize: _fontSize,
|
||||
height: 1.55,
|
||||
_cleanFormulas(section.title),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 15.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...section.lines.map((line) => _buildSectionLine(line, _fontSize)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -135,10 +135,10 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
icon: Icon(CupertinoIcons.money_dollar_circle_fill, size: 18),
|
||||
text: 'حاسبة المواريث والتجويد ⚖️',
|
||||
);
|
||||
} else if (_isHistorySubject || norm == 'تاريخ الأردن') {
|
||||
} else if (_isHistorySubject || norm == 'تاريخ الأردن' || norm == 'التاريخ') {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.time_solid, size: 18),
|
||||
text: 'الخط الزمني لتاريخ الأردن 🏛️',
|
||||
text: 'الخط الزمني الشامل والتاريخ 🏛️',
|
||||
);
|
||||
} else if (_isChemistrySubject || norm == 'الكيمياء') {
|
||||
return const Tab(
|
||||
@@ -165,43 +165,59 @@ class _SubjectHubScreenState extends State<SubjectHubScreen>
|
||||
icon: Icon(CupertinoIcons.desktopcomputer, size: 18),
|
||||
text: 'مختبر الخوارزميات والبرمجة 💻',
|
||||
);
|
||||
} else {
|
||||
} else if (norm == 'الجغرافيا') {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.map, size: 18),
|
||||
text: 'مختبر الجغرافيا والخرائط 🗺️',
|
||||
);
|
||||
} else if (norm == 'التربية الوطنية والمدنية') {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.person_3_fill, size: 18),
|
||||
text: 'مختبر المواطنة والمجتمع 🏛️',
|
||||
);
|
||||
} else if (_isPhysicsSubject || norm == 'الفيزياء') {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.bolt_fill, size: 18),
|
||||
text: 'مختبر الفيزياء والميكانيكا ⚡',
|
||||
);
|
||||
} else {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.lab_flask_solid, size: 18),
|
||||
text: 'المختبر الافتراضي التفاعلي 🧪',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget get _dynamicLabWidget {
|
||||
Widget? specializedTool;
|
||||
String? specializedTitle;
|
||||
final norm = Grade10LabsRegistry.normalizeSubject(widget.subject.title);
|
||||
|
||||
if (_isMathSubject) {
|
||||
if (_isMathSubject || norm == 'الرياضيات') {
|
||||
specializedTool = const MathInteractiveLabView();
|
||||
specializedTitle = 'معمل جيوجبرا 📐';
|
||||
} else if (_isEnglishSubject) {
|
||||
} else if (_isEnglishSubject || norm == 'اللغة الإنجليزية') {
|
||||
specializedTool = const EnglishInteractiveLabView();
|
||||
specializedTitle = 'مدرب الصوتيات 🎙️';
|
||||
} else if (_isArabicSubject) {
|
||||
} else if (_isArabicSubject || norm == 'العربية لغتي') {
|
||||
specializedTool = const ArabicInteractiveLabView();
|
||||
specializedTitle = 'شجرة الإعراب 📜';
|
||||
} else if (_isIslamicSubject) {
|
||||
} else if (_isIslamicSubject || norm == 'التربية الإسلامية') {
|
||||
specializedTool = const IslamicInteractiveLabView();
|
||||
specializedTitle = 'حاسبة المواريث ⚖️';
|
||||
} else if (_isHistorySubject) {
|
||||
} else if (_isHistorySubject || norm == 'تاريخ الأردن' || norm == 'التاريخ') {
|
||||
specializedTool = const HistoryInteractiveTimelineView();
|
||||
specializedTitle = 'الخط الزمني الشامل 🏛️';
|
||||
} else if (_isChemistrySubject) {
|
||||
} else if (_isChemistrySubject || norm == 'الكيمياء') {
|
||||
specializedTool = const ChemistryInteractiveLabView();
|
||||
specializedTitle = 'طيف ذرة بور ⚗️';
|
||||
} else if (_isBiologySubject) {
|
||||
} else if (_isBiologySubject || norm == 'العلوم الحياتية') {
|
||||
specializedTool = const BiologyInteractiveLabView();
|
||||
specializedTitle = 'المجهر الرقمي 🔬';
|
||||
} else if (_isEarthSciencesSubject) {
|
||||
} else if (_isEarthSciencesSubject || norm == 'علوم الأرض والبيئة') {
|
||||
specializedTool = const EarthSciencesInteractiveLabView();
|
||||
specializedTitle = 'دورة الصخور والطبقات 🌍';
|
||||
} else if (_isPhysicsSubject) {
|
||||
} else if (_isPhysicsSubject || norm == 'الفيزياء') {
|
||||
specializedTool = const PhysicsInteractiveLabView();
|
||||
specializedTitle = 'محاكاة المتجهات ⚡';
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../../core/config/app_config.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../../logic/cubits/auth_cubit.dart';
|
||||
@@ -81,16 +82,24 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
}
|
||||
|
||||
final token = await StorageService().getToken();
|
||||
final headers = <String, String>{};
|
||||
if (token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
String finalVideoUrl = videoUrl;
|
||||
if (token != null && token.isNotEmpty && finalVideoUrl.contains('/api/videos/') && !finalVideoUrl.contains('token=')) {
|
||||
|
||||
// IMPORTANT: Check whether the URL points to our own Saqel API or to an external CDN (BunnyCDN, Cloudflare R2).
|
||||
// NEVER send the Saqel user JWT Bearer token to external CDNs! CDNs reject unauthorized Bearer headers
|
||||
// with HTTP 403 Forbidden ("Resource is protected by access token").
|
||||
final isOurBackend = finalVideoUrl.contains('/api/videos/') ||
|
||||
finalVideoUrl.startsWith(AppConfig.baseUrl) ||
|
||||
finalVideoUrl.contains('localhost') ||
|
||||
finalVideoUrl.contains('127.0.0.1');
|
||||
|
||||
final headers = <String, String>{};
|
||||
if (isOurBackend && token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
if (!finalVideoUrl.contains('token=')) {
|
||||
final sep = finalVideoUrl.contains('?') ? '&' : '?';
|
||||
finalVideoUrl = '$finalVideoUrl${sep}token=${Uri.encodeComponent(token)}';
|
||||
}
|
||||
}
|
||||
|
||||
_videoController?.dispose();
|
||||
_videoController = VideoPlayerController.networkUrl(
|
||||
@@ -119,6 +128,38 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
}
|
||||
}
|
||||
}).catchError((err) {
|
||||
final errStr = err.toString();
|
||||
// Resilient Fallback: If 403 Forbidden or Access Token error occurred on an external URL with token query params,
|
||||
// retry once without the token parameters (e.g. public pull zones or direct CDNs).
|
||||
if ((errStr.contains('403') || errStr.contains('token') || errStr.contains('permission')) &&
|
||||
finalVideoUrl.contains('?')) {
|
||||
final strippedUrl = finalVideoUrl.split('?').first;
|
||||
if (strippedUrl != finalVideoUrl && strippedUrl.isNotEmpty) {
|
||||
_videoController?.dispose();
|
||||
_videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(strippedUrl),
|
||||
httpHeaders: const {},
|
||||
)..initialize().then((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = true;
|
||||
_videoInitError = null;
|
||||
});
|
||||
_revealVideoControls();
|
||||
if (shouldPlay) _videoController!.play();
|
||||
}
|
||||
}).catchError((retryErr) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = false;
|
||||
_videoInitError = 'تعذر تشغيل بث الفيديو من المصدر السحابي، يمكنك استخدام السبورة التفاعلية للشرح.';
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = false;
|
||||
@@ -407,13 +448,18 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.arrow_clockwise, size: 16),
|
||||
label: const Text('بدء تشغيل بث الفيديو المباشر 🎥'),
|
||||
label: const Text('إعادة محاولة البث 🎥'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.saqelCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
@@ -428,6 +474,23 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
);
|
||||
},
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(CupertinoIcons.mic_fill, size: 16, color: AppColors.appleBlue),
|
||||
label: const Text('السبورة والشرح الصوتي 🎙️', style: TextStyle(color: Colors.white)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColors.appleBlue.withAlpha(150)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showChalkboardMode = true;
|
||||
_videoInitError = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -586,7 +586,7 @@ const LabIdentity kFinanceBudgetToolIdentity = LabIdentity(
|
||||
|
||||
const LabIdentity kHistoryJordanChronologyToolIdentity = LabIdentity(
|
||||
toolKey: 'history_jordan_chronology',
|
||||
subjectAr: 'تاريخ الأردن',
|
||||
subjectAr: 'التاريخ',
|
||||
lessonAr: 'التسلسل الزمني التفاعلي',
|
||||
);
|
||||
|
||||
|
||||
@@ -339,8 +339,12 @@ class Grade10LabsRegistry {
|
||||
if (s.contains('وطنية') || s.contains('مدنية') || s.contains('civics')) {
|
||||
return 'التربية الوطنية والمدنية';
|
||||
}
|
||||
if (s.contains('أردن') || s.contains('اردن')) return 'تاريخ الأردن';
|
||||
if (s.contains('تاريخ') || s.contains('history')) return 'التاريخ';
|
||||
if (s.contains('تاريخ') ||
|
||||
s.contains('أردن') ||
|
||||
s.contains('اردن') ||
|
||||
s.contains('history')) {
|
||||
return 'التاريخ';
|
||||
}
|
||||
if (s.contains('رقمية') || s.contains('حاسوب') || s.contains('digital')) {
|
||||
return 'المهارات الرقمية';
|
||||
}
|
||||
@@ -353,7 +357,11 @@ class Grade10LabsRegistry {
|
||||
final direct = bySubject(norm);
|
||||
if (direct.isNotEmpty) return direct;
|
||||
return all
|
||||
.where((e) => e.subjectAr.contains(norm) || norm.contains(e.subjectAr))
|
||||
.where((e) =>
|
||||
e.subjectAr == norm ||
|
||||
(norm == 'التاريخ' && (e.subjectAr == 'التاريخ' || e.subjectAr == 'تاريخ الأردن')) ||
|
||||
e.subjectAr.contains(norm) ||
|
||||
norm.contains(e.subjectAr))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -361,9 +369,14 @@ class Grade10LabsRegistry {
|
||||
/// Defaults to true in development so that the reviewer/teacher can immediately test the labs.
|
||||
static bool previewModeEnabled = true;
|
||||
|
||||
/// Exact identity lookup.
|
||||
/// Exact identity lookup strictly isolated per subject.
|
||||
/// When [previewModeEnabled] is active or [allowPreview] is true, returns the lab for testing.
|
||||
/// When false (strict production gate), returns ONLY when released & carrying a published bundle.
|
||||
///
|
||||
/// CRITICAL INTEGRITY RULE:
|
||||
/// Never leak labs across subjects. A Physics lab must NEVER appear under History, Geography,
|
||||
/// Digital Skills, Civics, or any other subject. If a subject has no matching candidate lab,
|
||||
/// this method returns null immediately.
|
||||
static Grade10LabEntry? findForLesson({
|
||||
required String subjectTitle,
|
||||
required String lessonTitle,
|
||||
@@ -372,43 +385,56 @@ class Grade10LabsRegistry {
|
||||
bool? allowPreview,
|
||||
}) {
|
||||
final usePreview = allowPreview ?? previewModeEnabled;
|
||||
if (usePreview) {
|
||||
if (curriculumLessonId != null && curriculumLessonId.isNotEmpty) {
|
||||
final exact = entryForCurriculumLessonId(curriculumLessonId);
|
||||
if (exact != null) return exact;
|
||||
}
|
||||
if (lessonId != null && lessonId.isNotEmpty) {
|
||||
for (final e in all) {
|
||||
if (e.identity.curriculumLessonId.contains(lessonId) ||
|
||||
e.identity.toolKey == lessonId ||
|
||||
e.identity.sourceMarkdown.contains(lessonId)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
final candidates = bySubjectNormalized(subjectTitle);
|
||||
if (candidates.isNotEmpty) {
|
||||
final t = lessonTitle.toLowerCase();
|
||||
|
||||
// Subject Isolation Gate: If no candidate labs exist for this subject, return null immediately.
|
||||
if (candidates.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (usePreview) {
|
||||
// 1. Exact match on curriculumLessonId within candidate subject labs
|
||||
if (curriculumLessonId != null && curriculumLessonId.isNotEmpty) {
|
||||
for (final c in candidates) {
|
||||
if (t.contains(c.lessonAr.toLowerCase()) ||
|
||||
c.lessonAr.toLowerCase().contains(t)) {
|
||||
if (c.identity.curriculumLessonId == curriculumLessonId) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Lesson Title fuzzy matching strictly within candidate subject labs
|
||||
final t = lessonTitle.toLowerCase().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.curriculumLessonId.endsWith(lessonId)) {
|
||||
c.identity.toolKey == lessonId ||
|
||||
c.identity.curriculumLessonId.endsWith(lessonId) ||
|
||||
c.identity.sourceMarkdown.contains(lessonId)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (curriculumLessonId != null && curriculumLessonId.isNotEmpty) {
|
||||
for (final c in candidates) {
|
||||
if (c.identity.curriculumLessonId == curriculumLessonId && c.identity.isPublished) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return publishedEntryForCurriculumLessonId(curriculumLessonId ?? '');
|
||||
}
|
||||
|
||||
static Grade10LabEntry? entryForCurriculumLessonId(
|
||||
String curriculumLessonId) {
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:student_app/presentation/screens/virtual_labs/labs_registry.dart
|
||||
import 'package:student_app/presentation/screens/virtual_labs/subject_virtual_labs_view.dart';
|
||||
|
||||
void main() {
|
||||
test('registry covers all Grade-10 labs (48 entries, 14 subjects)', () {
|
||||
test('registry covers all Grade-10 labs (48 entries, 13 subjects)', () {
|
||||
expect(Grade10LabsRegistry.all.length, 48);
|
||||
expect(Grade10LabsRegistry.subjects.length, 14);
|
||||
expect(Grade10LabsRegistry.subjects.length, 13);
|
||||
final ids = Grade10LabsRegistry.all.map((e) => e.id).toSet();
|
||||
expect(ids.length, 48); // stable unique ids
|
||||
});
|
||||
@@ -98,6 +98,37 @@ void main() {
|
||||
'arabic_10_semester_1_unit_01_lesson_01');
|
||||
});
|
||||
|
||||
test('findForLesson strictly enforces subject isolation (no physics leak to History/Digital)', () {
|
||||
Grade10LabsRegistry.previewModeEnabled = true;
|
||||
|
||||
// History lesson 2 must NEVER return physics vector lab
|
||||
final historyLab = Grade10LabsRegistry.findForLesson(
|
||||
subjectTitle: 'تاريخ الأردن (Jordan History 10)',
|
||||
lessonTitle: 'الدرس الثاني: الإمبراطورية الساسانية',
|
||||
lessonId: 'lesson_02',
|
||||
);
|
||||
expect(historyLab, isNull);
|
||||
|
||||
// Digital skills lesson 1 must NEVER return physics vectors intro lab
|
||||
final digitalLab = Grade10LabsRegistry.findForLesson(
|
||||
subjectTitle: 'المهارات الرقمية (Digital Skills 10)',
|
||||
lessonTitle: 'معالجة البيانات',
|
||||
lessonId: 'lesson_01',
|
||||
);
|
||||
expect(digitalLab, isNull);
|
||||
|
||||
// History lesson 1 MUST return Persian Empire lab, NOT physics!
|
||||
final historyUnit1Lab = Grade10LabsRegistry.findForLesson(
|
||||
subjectTitle: 'تاريخ الأردن (Jordan History 10)',
|
||||
lessonTitle: 'الدرس الأول: الإمبراطورية الفارسية: النشأة والتطور',
|
||||
lessonId: 'lesson_01',
|
||||
curriculumLessonId: 'history_10_semester_1_unit_01_lesson_01',
|
||||
);
|
||||
expect(historyUnit1Lab, isNotNull);
|
||||
expect(historyUnit1Lab?.identity.curriculumLessonId, 'history_10_semester_1_unit_01_lesson_01');
|
||||
expect(historyUnit1Lab?.subjectAr, 'التاريخ');
|
||||
});
|
||||
|
||||
test('a lab is published only when released AND bundle version provided', () {
|
||||
// Emulate a server bundle arriving only for released bound lesson.
|
||||
const released = LabIdentity(
|
||||
|
||||
@@ -340,10 +340,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
version: "0.12.18"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -356,10 +356,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.17.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -585,10 +585,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.9"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -491,8 +491,8 @@ class VideoService
|
||||
return [
|
||||
'library_id' => getenv('BUNNY_STREAM_LIBRARY_ID') ?: '',
|
||||
'api_key' => getenv('BUNNY_STREAM_API_KEY') ?: '',
|
||||
'token_key' => getenv('BUNNY_STREAM_TOKEN_KEY') ?: getenv('JWT_SECRET') ?: '',
|
||||
'pull_zone_host' => getenv('BUNNY_STREAM_HOST') ?: '',
|
||||
'token_key' => getenv('BUNNY_STREAM_TOKEN_KEY') ?: '',
|
||||
'pull_zone_host' => getenv('BUNNY_STREAM_HOST') ?: 'video.bunnycdn.com',
|
||||
'embed_host' => 'iframe.mediadelivery.net'
|
||||
];
|
||||
}
|
||||
@@ -598,18 +598,29 @@ class VideoService
|
||||
$config = self::getBunnyConfig();
|
||||
$libraryId = $config['library_id'];
|
||||
$tokenKey = $config['token_key'];
|
||||
$expires = time() + $expiresInSeconds;
|
||||
$host = !empty($config['pull_zone_host']) ? $config['pull_zone_host'] : 'video.bunnycdn.com';
|
||||
|
||||
if (!empty($tokenKey)) {
|
||||
$expires = time() + $expiresInSeconds;
|
||||
$hashable = $tokenKey . $videoId . $expires;
|
||||
$token = hash('sha256', $hashable);
|
||||
|
||||
$embedUrl = "https://{$config['embed_host']}/embed/{$libraryId}/{$videoId}?token={$token}&expires={$expires}";
|
||||
$hlsUrl = "https://{$config['pull_zone_host']}/{$videoId}/playlist.m3u8?token={$token}&expires={$expires}";
|
||||
$thumbUrl = "https://{$config['pull_zone_host']}/{$videoId}/thumbnail.jpg?token={$token}&expires={$expires}";
|
||||
$hlsUrl = "https://{$host}/{$videoId}/playlist.m3u8?token={$token}&expires={$expires}";
|
||||
$thumbUrl = "https://{$host}/{$videoId}/thumbnail.jpg?token={$token}&expires={$expires}";
|
||||
} else {
|
||||
// Clean direct streaming without bogus token hashes that cause 403
|
||||
$expires = null;
|
||||
$token = '';
|
||||
$embedUrl = "https://{$config['embed_host']}/embed/{$libraryId}/{$videoId}";
|
||||
$hlsUrl = "https://{$host}/{$videoId}/playlist.m3u8";
|
||||
$thumbUrl = "https://{$host}/{$videoId}/thumbnail.jpg";
|
||||
}
|
||||
|
||||
return [
|
||||
'video_id' => $videoId,
|
||||
'library_id' => $libraryId,
|
||||
'video_url' => $hlsUrl,
|
||||
'embed_url' => $embedUrl,
|
||||
'hls_url' => $hlsUrl,
|
||||
'thumb_url' => $thumbUrl,
|
||||
|
||||
Reference in New Issue
Block a user