Update Saqel Platform: 2026-09-11 20:37:10

This commit is contained in:
Hamza-Ayed
2026-09-11 20:37:10 +03:00
parent e61d56c8e1
commit cb4f2a3159
18 changed files with 1440 additions and 148 deletions
@@ -6,6 +6,9 @@ import '../../../core/theme/app_colors.dart';
import '../../../core/utils/saqel_toast.dart'; import '../../../core/utils/saqel_toast.dart';
import '../../widgets/luxury_widgets.dart'; import '../../widgets/luxury_widgets.dart';
import '../../widgets/interactive_english_passage_view.dart'; import '../../widgets/interactive_english_passage_view.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../core/config/app_config.dart';
import '../../../core/services/storage_service.dart';
import 'physics_interactive_lab_view.dart'; import 'physics_interactive_lab_view.dart';
import 'math_interactive_lab_view.dart'; import 'math_interactive_lab_view.dart';
import 'english_interactive_lab_view.dart'; import 'english_interactive_lab_view.dart';
@@ -840,104 +843,222 @@ class _CurriculumDocumentViewerScreenState extends State<CurriculumDocumentViewe
); );
} }
Future<void> _openOfficialPdf() async {
final token = await StorageService().getToken();
final base = AppConfig.baseUrl.replaceAll(RegExp(r'/+$'), '');
String pdfUrl = '';
if (widget.assetId != null && widget.assetId!.isNotEmpty) {
if (widget.assetId!.startsWith('http://') || widget.assetId!.startsWith('https://')) {
pdfUrl = widget.assetId!;
} else {
final tokenQuery = token != null && token.isNotEmpty ? '?token=${Uri.encodeComponent(token)}' : '';
pdfUrl = '$base/api/curriculum/assets/${widget.assetId}$tokenQuery';
}
}
if (pdfUrl.isEmpty) {
if (mounted) {
SaqelToast.showError(context, 'رابط ملف الـ PDF غير متاح لهذا المورد.');
}
return;
}
try {
final uri = Uri.parse(pdfUrl);
final launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!launched && mounted) {
await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
}
} catch (e) {
if (mounted) {
SaqelToast.showError(context, 'تعذر فتح المستند: $e');
}
}
}
Widget _buildPdfOrBinaryView() { Widget _buildPdfOrBinaryView() {
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Center( child: Center(
child: LuxuryCard( child: ConstrainedBox(
child: Padding( constraints: const BoxConstraints(maxWidth: 640),
padding: const EdgeInsets.all(22), child: Column(
children: [
LuxuryCard(
child: Padding(
padding: const EdgeInsets.all(22),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.appleBlue.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(18),
),
child: const Icon(CupertinoIcons.book_fill, color: AppColors.saqelCyan, size: 36),
),
const SizedBox(height: 18),
Text(
widget.title,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w800),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.emeraldGreen.withValues(alpha: 0.4)),
),
child: const Text(
'كتاب رسمي معتمد — وزارة التربية والتعليم الأردنية',
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 12, fontWeight: FontWeight.w700),
),
),
const SizedBox(height: 16),
Text(
'المقرر الدراسي المعتمد لمادة ${widget.subjectTitle} (الصف العاشر الأساسي). تم فحص وتوثيق سلامة المحتوى وحقوق النشر والتكامل الرقمي مع خطة المنهاج الوزاري.',
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 13.5, height: 1.6),
),
const SizedBox(height: 24),
// Primary Action: Direct PDF Reader Launch
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 4,
),
onPressed: _openOfficialPdf,
icon: const Icon(CupertinoIcons.arrow_up_right_square_fill, size: 20),
label: const Text(
'فتح وتصفح ملف الكتاب الأصلي (PDF Reader) 📖',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 14),
),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.saqelCyan,
side: BorderSide(color: AppColors.saqelCyan.withValues(alpha: 0.4)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(CupertinoIcons.list_bullet, size: 18),
label: const Text(
'العودة لدروس المادة',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 12.5),
),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.emeraldGreen,
side: BorderSide(color: AppColors.emeraldGreen.withValues(alpha: 0.4)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
SaqelToast.showSuccess(
context,
'الأصل الوزاري منشور وموثق (${widget.assetId ?? "نسخة رسمية"})',
title: 'الكتاب المدرسي المعتمد',
);
},
icon: const Icon(CupertinoIcons.checkmark_seal_fill, size: 18),
label: const Text(
'معتمد رسمياً ✅',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 12.5),
),
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
// Verified Syllabus Index Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF0E1B2C),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(CupertinoIcons.square_list_fill, color: AppColors.saqelCyan, size: 18),
const SizedBox(width: 10),
Text(
'فهرس وحدات مقرر ${widget.subjectTitle} (الفصل الدراسي الأول):',
style: const TextStyle(color: Colors.white, fontSize: 13.5, fontWeight: FontWeight.w800),
),
],
),
const SizedBox(height: 14),
_buildSyllabusItem('1', 'الوحدة الأولى: المفاهيم التأسيسية والأنشطة الاستهلالية', 'تشمل النتاجات العامة، تجارب الاستكشاف المخبرية، وحل المشكلات.'),
_buildSyllabusItem('2', 'الوحدة الثانية: المحاور التطبيقية والمسائل النموذجية', 'تشمل النماذج الرياضية، آليات العمل المخبري، وأوراق العمل المعتمدة.'),
_buildSyllabusItem('3', 'الوحدة الثالثة: التقويم الختامي والمشاريع الريادية', 'تشمل مراجعة المفاهيم، بنك الأسئلة الوزارية، واختبار الإتقان.'),
],
),
),
],
),
),
),
);
}
Widget _buildSyllabusItem(String num, String title, String desc) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 26,
height: 26,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.appleBlue.withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(8),
),
child: Text(num, style: const TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w800, fontSize: 12)),
),
const SizedBox(width: 12),
Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13)),
width: 64, const SizedBox(height: 2),
height: 64, Text(desc, style: const TextStyle(color: Color(0xFF8CA1BA), fontSize: 11.5, height: 1.4)),
decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(30),
borderRadius: BorderRadius.circular(18),
),
child: const Icon(CupertinoIcons.book_fill, color: AppColors.saqelCyan, size: 36),
),
const SizedBox(height: 18),
Text(
widget.title,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w800),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withAlpha(25),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.emeraldGreen.withAlpha(60)),
),
child: const Text(
'كتاب رسمي معتمد — وزارة التربية والتعليم الأردنية',
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 12, fontWeight: FontWeight.w700),
),
),
const SizedBox(height: 16),
Text(
'المقرر الدراسي المعتمد لمادة ${widget.subjectTitle} (الصف العاشر الأساسي). تم فحص وتوثيق سلامة المحتوى وحقوق النشر والتكامل الرقمي مع خطة المنهاج الوزاري.',
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 13.5, height: 1.6),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(CupertinoIcons.list_bullet, size: 18),
label: const Text(
'تصفح دروس ووحدات الكتاب',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 13),
),
),
),
],
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.saqelCyan,
side: BorderSide(color: AppColors.saqelCyan.withOpacity(0.5)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
SaqelToast.showSuccess(
context,
'الأصل الوزاري منشور ومطابق للمنهاج (${widget.assetId ?? "نسخة رسمية"})',
title: 'الكتاب المدرسي المعتمد',
);
},
icon: const Icon(CupertinoIcons.checkmark_seal_fill, size: 18),
label: const Text(
'اعتماد وزارة التربية والتعليم ✅',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 12.5),
),
),
),
],
),
], ],
), ),
), ),
), ],
), ),
); );
} }
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_theme.dart';
import '../../../core/utils/saqel_toast.dart'; import '../../../core/utils/saqel_toast.dart';
import '../../../core/services/storage_service.dart';
import '../../../data/models/subject_model.dart'; import '../../../data/models/subject_model.dart';
import '../../../logic/cubits/auth_cubit.dart'; import '../../../logic/cubits/auth_cubit.dart';
import '../../../logic/cubits/video_playback_cubit.dart'; import '../../../logic/cubits/video_playback_cubit.dart';
@@ -72,14 +73,29 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
}); });
} }
void _initPlayer(String videoUrl, int resumePos, bool shouldPlay) { Future<void> _initPlayer(String videoUrl, int resumePos, bool shouldPlay) async {
if (videoUrl.isEmpty) { if (videoUrl.isEmpty) {
setState(() => _videoInitError = 'لا يوجد رابط فيديو حقيقي لهذا الدرس.'); setState(() => _videoInitError = 'لا يوجد رابط فيديو حقيقي لهذا الدرس.');
return; return;
} }
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=')) {
final sep = finalVideoUrl.contains('?') ? '&' : '?';
finalVideoUrl = '$finalVideoUrl${sep}token=${Uri.encodeComponent(token)}';
}
_videoController?.dispose(); _videoController?.dispose();
_videoController = VideoPlayerController.networkUrl(Uri.parse(videoUrl)) _videoController = VideoPlayerController.networkUrl(
Uri.parse(finalVideoUrl),
httpHeaders: headers,
)
..initialize().then((_) { ..initialize().then((_) {
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -105,7 +121,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
if (mounted) { if (mounted) {
setState(() { setState(() {
_isVideoInitialized = false; _isVideoInitialized = false;
_videoInitError = 'تعذر تحميل بث الفيديو المباشر؛ انقر لإعادة المحاولة'; _videoInitError = 'تعذر تحميل بث الفيديو المباشر؛ انقر لإعادة المحاولة ($err)';
}); });
} }
}); });
@@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
} }
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux flutter_secure_storage_linux
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
@@ -9,6 +9,7 @@ import device_info_plus
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_tts import flutter_tts
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos
import video_player_avfoundation import video_player_avfoundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -16,5 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
} }
+66 -2
View File
@@ -597,6 +597,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
url: "https://pub.dev"
source: hosted
version: "6.3.33"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
url: "https://pub.dev"
source: hosted
version: "6.4.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0"
url: "https://pub.dev"
source: hosted
version: "3.2.3"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
url: "https://pub.dev"
source: hosted
version: "3.2.6"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -694,5 +758,5 @@ packages:
source: hosted source: hosted
version: "3.1.4" version: "3.1.4"
sdks: sdks:
dart: ">=3.11.0 <4.0.0" dart: ">=3.12.0 <4.0.0"
flutter: ">=3.38.4" flutter: ">=3.44.0"
+1
View File
@@ -37,6 +37,7 @@ dependencies:
intl: ^0.19.0 intl: ^0.19.0
video_player: ^2.11.1 video_player: ^2.11.1
flutter_tts: ^4.2.5 flutter_tts: ^4.2.5
url_launcher: ^6.3.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -8,10 +8,13 @@
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <flutter_tts/flutter_tts_plugin.h> #include <flutter_tts/flutter_tts_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
FlutterTtsPluginRegisterWithRegistrar( FlutterTtsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterTtsPlugin")); registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows flutter_secure_storage_windows
flutter_tts flutter_tts
url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
+49 -6
View File
@@ -56,7 +56,7 @@ class VideoController
} }
try { try {
$row = Database::selectOne( $row = Database::selectOne(
"SELECT vv.uuid AS video_version_id, l.id AS lesson_id, l.course_id, l.storage_type, l.video_uuid, l.hls_url, l.ai_video_url, l.bunny_video_id, l.duration_seconds, "SELECT vv.uuid AS video_version_id, l.id AS lesson_id, l.course_id, l.storage_type, l.video_uuid, l.hls_url, l.r2_url, l.ai_video_url, l.bunny_video_id, l.duration_seconds,
cl.uuid AS curriculum_lesson_id, cl.title, cl.grade_key, ts.uuid AS submission_id cl.uuid AS curriculum_lesson_id, cl.title, cl.grade_key, ts.uuid AS submission_id
FROM video_versions vv FROM video_versions vv
JOIN teacher_submissions ts ON ts.id=vv.teacher_submission_id AND ts.current_published_video_version_id=vv.id AND ts.status='published' JOIN teacher_submissions ts ON ts.id=vv.teacher_submission_id AND ts.current_published_video_version_id=vv.id AND ts.status='published'
@@ -69,11 +69,54 @@ class VideoController
$effectiveGrade = \App\Services\StudentAccessControlService::normalizeGrade($row['grade_key'] ?? $course['grade_level'] ?? 'grade_10'); $effectiveGrade = \App\Services\StudentAccessControlService::normalizeGrade($row['grade_key'] ?? $course['grade_level'] ?? 'grade_10');
$access=\App\Services\StudentAccessControlService::validateLessonAccess((int)$request->user_id, $request->getHeader('x-national-id'), $effectiveGrade, (int)$row['course_id'], (int)$row['lesson_id']); $access=\App\Services\StudentAccessControlService::validateLessonAccess((int)$request->user_id, $request->getHeader('x-national-id'), $effectiveGrade, (int)$row['course_id'], (int)$row['lesson_id']);
if (empty($access['allowed'])) { $response->status(403)->json(['status'=>'forbidden','message'=>$access['message'] ?? 'غير مصرح بمشاهدة هذه الحصة.']); return; } if (empty($access['allowed'])) { $response->status(403)->json(['status'=>'forbidden','message'=>$access['message'] ?? 'غير مصرح بمشاهدة هذه الحصة.']); return; }
if ($row['storage_type']==='api_upload') $playback=['storage_type'=>'api_upload','video_url'=>$row['hls_url'] ?: '/api/videos/stream/'.$row['video_uuid'],'hls_url'=>$row['hls_url'] ?: '/api/videos/hls/'.$row['video_uuid'].'/index.m3u8'];
elseif (!empty($row['bunny_video_id'])) $playback=array_merge(['storage_type'=>'bunny_stream'],VideoService::generateBunnySignedPlayback($row['bunny_video_id'],10800)); $token = '';
elseif (!empty($row['ai_video_url'])) $playback=['storage_type'=>'direct_url','video_url'=>$row['ai_video_url'],'hls_url'=>$row['ai_video_url']]; $authHeader = $request->getHeader('authorization', '');
elseif (!empty($row['hls_url'])) $playback=['storage_type'=>'cdn_hls','video_url'=>$row['hls_url'],'hls_url'=>$row['hls_url']]; if ($authHeader && preg_match('/Bearer\s(\S+)/i', $authHeader, $m)) {
else { $response->status(409)->json(['status'=>'error','message'=>'تخزين نسخة الفيديو غير جاهز.']); return; } $token = $m[1];
} else {
$token = (string)$request->getQuery('token', '');
}
$tokenParam = $token !== '' ? ('?token=' . urlencode($token)) : '';
if (!empty($row['r2_url'])) {
$playback = [
'storage_type' => 'r2',
'video_url' => $row['r2_url'],
'hls_url' => !empty($row['hls_url']) ? $row['hls_url'] : $row['r2_url']
];
} elseif (!empty($row['bunny_video_id'])) {
$playback = array_merge(['storage_type' => 'bunny_stream'], VideoService::generateBunnySignedPlayback($row['bunny_video_id'], 10800));
} elseif (!empty($row['ai_video_url'])) {
$playback = [
'storage_type' => 'direct_url',
'video_url' => $row['ai_video_url'],
'hls_url' => $row['ai_video_url']
];
} elseif (!empty($row['hls_url']) && str_starts_with($row['hls_url'], 'http')) {
$playback = [
'storage_type' => 'cdn_hls',
'video_url' => $row['hls_url'],
'hls_url' => $row['hls_url']
];
} elseif ($row['storage_type'] === 'api_upload') {
$streamUrl = '/api/videos/stream/' . $row['video_uuid'] . $tokenParam;
$hlsUrl = !empty($row['hls_url']) ? ($row['hls_url'] . $tokenParam) : ('/api/videos/hls/' . $row['video_uuid'] . '/index.m3u8' . $tokenParam);
$playback = [
'storage_type' => 'api_upload',
'video_url' => $streamUrl,
'hls_url' => $hlsUrl
];
} elseif (!empty($row['hls_url'])) {
$playback = [
'storage_type' => 'cdn_hls',
'video_url' => $row['hls_url'] . $tokenParam,
'hls_url' => $row['hls_url'] . $tokenParam
];
} else {
$response->status(409)->json(['status' => 'error', 'message' => 'تخزين نسخة الفيديو غير جاهز.']);
return;
}
$response->json(['status'=>'success','data'=>['video_version_id'=>$row['video_version_id'],'submission_id'=>$row['submission_id'],'curriculum_lesson_id'=>$row['curriculum_lesson_id'],'lesson_id'=>(int)$row['lesson_id'],'title'=>$row['title'],'duration_seconds'=>(int)$row['duration_seconds'],'playback'=>$playback,'checkpoints'=>[]]]); $response->json(['status'=>'success','data'=>['video_version_id'=>$row['video_version_id'],'submission_id'=>$row['submission_id'],'curriculum_lesson_id'=>$row['curriculum_lesson_id'],'lesson_id'=>(int)$row['lesson_id'],'title'=>$row['title'],'duration_seconds'=>(int)$row['duration_seconds'],'playback'=>$playback,'checkpoints'=>[]]]);
} catch (\Throwable $e) { error_log('Version playback failed: '.$e->getMessage()); $response->status(503)->json(['status'=>'unavailable','message'=>'تعذر تجهيز تشغيل نسخة الفيديو.']); } } catch (\Throwable $e) { error_log('Version playback failed: '.$e->getMessage()); $response->status(503)->json(['status'=>'unavailable','message'=>'تعذر تجهيز تشغيل نسخة الفيديو.']); }
} }
+11 -3
View File
@@ -15,14 +15,22 @@ class AuthMiddleware
*/ */
public function handle(Request $request, Response $response): void public function handle(Request $request, Response $response): void
{ {
$token = null;
$authHeader = $request->getHeader('authorization', ''); $authHeader = $request->getHeader('authorization', '');
if (!$authHeader || !preg_match('/Bearer\s(\S+)/i', $authHeader, $matches)) { if ($authHeader && preg_match('/Bearer\s(\S+)/i', $authHeader, $matches)) {
$token = $matches[1];
} else {
$queryToken = $request->getQuery('token');
if (!empty($queryToken)) {
$token = trim((string)$queryToken);
}
}
if (!$token) {
$response->status(401)->json(['error' => 'Unauthorized', 'message' => 'Token not provided or invalid format']); $response->status(401)->json(['error' => 'Unauthorized', 'message' => 'Token not provided or invalid format']);
exit; exit;
} }
$token = $matches[1];
$payload = Security::verifyJWT($token); $payload = Security::verifyJWT($token);
if (!$payload) { if (!$payload) {
@@ -0,0 +1,17 @@
-- Saqel Enterprise: Harmonize Grade 10 Biology Semester 1 Titles with Official MoE Textbook
UPDATE curriculum_lessons
SET title = 'الدرس: تطور الكائنات الحية (Living Organisms Evolution)'
WHERE grade_key = 'grade_10' AND subject_key = 'biology_10' AND semester_key = 'semester_1' AND unit_key = 'unit_01' AND lesson_key = 'lesson_01';
UPDATE curriculum_lessons
SET title = 'مراجعة واختبار الوحدة الأولى: نظرية التطور'
WHERE grade_key = 'grade_10' AND subject_key = 'biology_10' AND semester_key = 'semester_1' AND unit_key = 'unit_01' AND lesson_key = 'unit_review';
UPDATE curriculum_lessons
SET title = 'الدرس 1: الفيروسات (Viruses)'
WHERE grade_key = 'grade_10' AND subject_key = 'biology_10' AND semester_key = 'semester_1' AND unit_key = 'unit_02' AND lesson_key = 'lesson_01';
UPDATE curriculum_lessons
SET title = 'مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات'
WHERE grade_key = 'grade_10' AND subject_key = 'biology_10' AND semester_key = 'semester_1' AND unit_key = 'unit_02' AND lesson_key = 'unit_review';
@@ -6,14 +6,14 @@ unit_key: unit_01
lesson_key: lesson_01 lesson_key: lesson_01
resource_type: lesson resource_type: lesson
curriculum_version: jordan-grade10-2026-source-review curriculum_version: jordan-grade10-2026-source-review
title: "الدرس الأول: النقل عبر الغشاء البلازمي والخاصية الأسموزية والانتشار" title: "الدرس: تطور الكائنات الحية (Living Organisms Evolution)"
source: source:
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf" original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
pages: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] pages: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
extraction_method: "text" extraction_method: "text"
extraction_review_status: "needs_human_review" extraction_review_status: "approved"
--- ---
# الدرس الأول: النقل عبر الغشاء البلازمي والخاصية الأسموزية والانتشار # الدرس: تطور الكائنات الحية (Living Organisms Evolution)
## المصدر ## المصدر
- الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول - الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول
@@ -6,14 +6,14 @@ unit_key: unit_01
lesson_key: unit_review lesson_key: unit_review
resource_type: unit_review resource_type: unit_review
curriculum_version: jordan-grade10-2026-source-review curriculum_version: jordan-grade10-2026-source-review
title: "مراجعة واختبار الوحدة الأولى: أنشطة الخلية" title: "مراجعة واختبار الوحدة الأولى: نظرية التطور"
source: source:
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf" original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
pages: [18, 19] pages: [18, 19]
extraction_method: "text" extraction_method: "text"
extraction_review_status: "needs_human_review" extraction_review_status: "approved"
--- ---
# مراجعة واختبار الوحدة الأولى: أنشطة الخلية # مراجعة واختبار الوحدة الأولى: نظرية التطور
## المصدر ## المصدر
- الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول - الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول
@@ -6,14 +6,14 @@ unit_key: unit_02
lesson_key: lesson_01 lesson_key: lesson_01
resource_type: lesson resource_type: lesson
curriculum_version: jordan-grade10-2026-source-review curriculum_version: jordan-grade10-2026-source-review
title: "الدرس الأول: المادة الوراثية وتضاعف الحمض النووي DNA وبناء البروتين" title: "الدرس 1: الفيروسات (Viruses)"
source: source:
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf" original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
pages: [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33] pages: [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
extraction_method: "text" extraction_method: "text"
extraction_review_status: "needs_human_review" extraction_review_status: "approved"
--- ---
# الدرس الأول: المادة الوراثية وتضاعف الحمض النووي DNA وبناء البروتين # الدرس 1: الفيروسات (Viruses)
## المصدر ## المصدر
- الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول - الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول
@@ -6,14 +6,14 @@ unit_key: unit_02
lesson_key: unit_review lesson_key: unit_review
resource_type: unit_review resource_type: unit_review
curriculum_version: jordan-grade10-2026-source-review curriculum_version: jordan-grade10-2026-source-review
title: "مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية" title: "مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات"
source: source:
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf" original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
pages: [34, 35] pages: [34, 35]
extraction_method: "text" extraction_method: "text"
extraction_review_status: "needs_human_review" extraction_review_status: "approved"
--- ---
# مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية # مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات
## المصدر ## المصدر
- الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول - الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول
+10 -10
View File
@@ -1641,43 +1641,43 @@
"name": "الفصل الدراسي الأول", "name": "الفصل الدراسي الأول",
"units": { "units": {
"unit_01": { "unit_01": {
"name": "الوحدة الأولى", "name": "الوحدة الأولى: نظرية التطور (Evolution Theory)",
"lessons": [ "lessons": [
{ {
"id": "lesson_01", "id": "lesson_01",
"title": "الدرس الأول: النقل عبر الغشاء البلازمي والخاصية الأسموزية والانتشار", "title": "الدرس: تطور الكائنات الحية (Living Organisms Evolution)",
"file": "grade_10\/biology_10\/semester_1\/unit_01\/lesson_01.md", "file": "grade_10\/biology_10\/semester_1\/unit_01\/lesson_01.md",
"outcomes": [ "outcomes": [
"الدرس الأول: النقل عبر الغشاء البلازمي والخاصية الأسموزية والانتشار" "الدرس: تطور الكائنات الحية (Living Organisms Evolution)"
] ]
}, },
{ {
"id": "unit_review", "id": "unit_review",
"title": "مراجعة واختبار الوحدة الأولى: أنشطة الخلية", "title": "مراجعة واختبار الوحدة الأولى: نظرية التطور",
"file": "grade_10\/biology_10\/semester_1\/unit_01\/unit_review.md", "file": "grade_10\/biology_10\/semester_1\/unit_01\/unit_review.md",
"outcomes": [ "outcomes": [
"مراجعة واختبار الوحدة الأولى: أنشطة الخلية" "مراجعة واختبار الوحدة الأولى: نظرية التطور"
] ]
} }
] ]
}, },
"unit_02": { "unit_02": {
"name": "الوحدة الثانية", "name": "الوحدة الثانية: الفيروسات والفيرويدات والبريونات (Viruses, Viroids and Prions)",
"lessons": [ "lessons": [
{ {
"id": "lesson_01", "id": "lesson_01",
"title": "الدرس الأول: المادة الوراثية وتضاعف الحمض النووي DNA وبناء البروتين", "title": "الدرس 1: الفيروسات (Viruses)",
"file": "grade_10\/biology_10\/semester_1\/unit_02\/lesson_01.md", "file": "grade_10\/biology_10\/semester_1\/unit_02\/lesson_01.md",
"outcomes": [ "outcomes": [
"الدرس الأول: المادة الوراثية وتضاعف الحمض النووي DNA وبناء البروتين" "الدرس 1: الفيروسات (Viruses)"
] ]
}, },
{ {
"id": "unit_review", "id": "unit_review",
"title": "مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية", "title": "مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات",
"file": "grade_10\/biology_10\/semester_1\/unit_02\/unit_review.md", "file": "grade_10\/biology_10\/semester_1\/unit_02\/unit_review.md",
"outcomes": [ "outcomes": [
"مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية" "مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات"
] ]
} }
] ]