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 '../../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)),
],
),
),
),
],
),
);
}
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"))
}
+66 -2
View File
@@ -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"
+1
View File
@@ -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