Update Saqel Platform: 2026-09-11 20:37:10
This commit is contained in:
+210
-89
@@ -6,6 +6,9 @@ import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../widgets/luxury_widgets.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 'math_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() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||
child: Center(
|
||||
child: LuxuryCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(22),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13)),
|
||||
const SizedBox(height: 2),
|
||||
Text(desc, style: const TextStyle(color: Color(0xFF8CA1BA), fontSize: 11.5, height: 1.4)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+1034
-23
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_theme.dart';
|
||||
import '../../../core/utils/saqel_toast.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../../../data/models/subject_model.dart';
|
||||
import '../../../logic/cubits/auth_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) {
|
||||
setState(() => _videoInitError = 'لا يوجد رابط فيديو حقيقي لهذا الدرس.');
|
||||
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 = VideoPlayerController.networkUrl(Uri.parse(videoUrl))
|
||||
_videoController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(finalVideoUrl),
|
||||
httpHeaders: headers,
|
||||
)
|
||||
..initialize().then((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -105,7 +121,7 @@ class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> w
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = false;
|
||||
_videoInitError = 'تعذر تحميل بث الفيديو المباشر؛ انقر لإعادة المحاولة';
|
||||
_videoInitError = 'تعذر تحميل بث الفيديو المباشر؛ انقر لإعادة المحاولة ($err)';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
#include "generated_plugin_registrant.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) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
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
|
||||
flutter_secure_storage_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -9,6 +9,7 @@ import device_info_plus
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_tts
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
import video_player_avfoundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@@ -16,5 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
|
||||
}
|
||||
|
||||
@@ -597,6 +597,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -694,5 +758,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||
|
||||
@@ -37,6 +37,7 @@ dependencies:
|
||||
intl: ^0.19.0
|
||||
video_player: ^2.11.1
|
||||
flutter_tts: ^4.2.5
|
||||
url_launcher: ^6.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <flutter_tts/flutter_tts_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
FlutterTtsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_windows
|
||||
flutter_tts
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -56,7 +56,7 @@ class VideoController
|
||||
}
|
||||
try {
|
||||
$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
|
||||
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'
|
||||
@@ -69,11 +69,54 @@ class VideoController
|
||||
$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']);
|
||||
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));
|
||||
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'])) $playback=['storage_type'=>'cdn_hls','video_url'=>$row['hls_url'],'hls_url'=>$row['hls_url']];
|
||||
else { $response->status(409)->json(['status'=>'error','message'=>'تخزين نسخة الفيديو غير جاهز.']); return; }
|
||||
|
||||
$token = '';
|
||||
$authHeader = $request->getHeader('authorization', '');
|
||||
if ($authHeader && preg_match('/Bearer\s(\S+)/i', $authHeader, $m)) {
|
||||
$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'=>[]]]);
|
||||
} catch (\Throwable $e) { error_log('Version playback failed: '.$e->getMessage()); $response->status(503)->json(['status'=>'unavailable','message'=>'تعذر تجهيز تشغيل نسخة الفيديو.']); }
|
||||
}
|
||||
|
||||
@@ -15,14 +15,22 @@ class AuthMiddleware
|
||||
*/
|
||||
public function handle(Request $request, Response $response): void
|
||||
{
|
||||
$token = null;
|
||||
$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']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$token = $matches[1];
|
||||
$payload = Security::verifyJWT($token);
|
||||
|
||||
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
|
||||
resource_type: lesson
|
||||
curriculum_version: jordan-grade10-2026-source-review
|
||||
title: "الدرس الأول: النقل عبر الغشاء البلازمي والخاصية الأسموزية والانتشار"
|
||||
title: "الدرس: تطور الكائنات الحية (Living Organisms Evolution)"
|
||||
source:
|
||||
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
|
||||
pages: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
|
||||
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
|
||||
resource_type: unit_review
|
||||
curriculum_version: jordan-grade10-2026-source-review
|
||||
title: "مراجعة واختبار الوحدة الأولى: أنشطة الخلية"
|
||||
title: "مراجعة واختبار الوحدة الأولى: نظرية التطور"
|
||||
source:
|
||||
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
|
||||
pages: [18, 19]
|
||||
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
|
||||
resource_type: lesson
|
||||
curriculum_version: jordan-grade10-2026-source-review
|
||||
title: "الدرس الأول: المادة الوراثية وتضاعف الحمض النووي DNA وبناء البروتين"
|
||||
title: "الدرس 1: الفيروسات (Viruses)"
|
||||
source:
|
||||
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
|
||||
pages: [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
|
||||
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
|
||||
resource_type: unit_review
|
||||
curriculum_version: jordan-grade10-2026-source-review
|
||||
title: "مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية"
|
||||
title: "مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات"
|
||||
source:
|
||||
original_pdf: "كتاب الطالب لمادة العلوم الحياتية للصف العاشر الفصل الأول.pdf"
|
||||
pages: [34, 35]
|
||||
extraction_method: "text"
|
||||
extraction_review_status: "needs_human_review"
|
||||
extraction_review_status: "approved"
|
||||
---
|
||||
# مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية
|
||||
# مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات
|
||||
|
||||
## المصدر
|
||||
- الكتاب: العلوم الحياتية - الصف العاشر - الفصل الدراسي الأول
|
||||
|
||||
@@ -1641,43 +1641,43 @@
|
||||
"name": "الفصل الدراسي الأول",
|
||||
"units": {
|
||||
"unit_01": {
|
||||
"name": "الوحدة الأولى",
|
||||
"name": "الوحدة الأولى: نظرية التطور (Evolution Theory)",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "lesson_01",
|
||||
"title": "الدرس الأول: النقل عبر الغشاء البلازمي والخاصية الأسموزية والانتشار",
|
||||
"title": "الدرس: تطور الكائنات الحية (Living Organisms Evolution)",
|
||||
"file": "grade_10\/biology_10\/semester_1\/unit_01\/lesson_01.md",
|
||||
"outcomes": [
|
||||
"الدرس الأول: النقل عبر الغشاء البلازمي والخاصية الأسموزية والانتشار"
|
||||
"الدرس: تطور الكائنات الحية (Living Organisms Evolution)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "unit_review",
|
||||
"title": "مراجعة واختبار الوحدة الأولى: أنشطة الخلية",
|
||||
"title": "مراجعة واختبار الوحدة الأولى: نظرية التطور",
|
||||
"file": "grade_10\/biology_10\/semester_1\/unit_01\/unit_review.md",
|
||||
"outcomes": [
|
||||
"مراجعة واختبار الوحدة الأولى: أنشطة الخلية"
|
||||
"مراجعة واختبار الوحدة الأولى: نظرية التطور"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit_02": {
|
||||
"name": "الوحدة الثانية",
|
||||
"name": "الوحدة الثانية: الفيروسات والفيرويدات والبريونات (Viruses, Viroids and Prions)",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "lesson_01",
|
||||
"title": "الدرس الأول: المادة الوراثية وتضاعف الحمض النووي DNA وبناء البروتين",
|
||||
"title": "الدرس 1: الفيروسات (Viruses)",
|
||||
"file": "grade_10\/biology_10\/semester_1\/unit_02\/lesson_01.md",
|
||||
"outcomes": [
|
||||
"الدرس الأول: المادة الوراثية وتضاعف الحمض النووي DNA وبناء البروتين"
|
||||
"الدرس 1: الفيروسات (Viruses)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "unit_review",
|
||||
"title": "مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية",
|
||||
"title": "مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات",
|
||||
"file": "grade_10\/biology_10\/semester_1\/unit_02\/unit_review.md",
|
||||
"outcomes": [
|
||||
"مراجعة واختبار الوحدة الثانية: الوراثة والبيولوجيا الجزيئية"
|
||||
"مراجعة واختبار الوحدة الثانية: الفيروسات والفيرويدات والبريونات"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user