Enhance video playback cubit resilience and K-12 curriculum flow

This commit is contained in:
Hamza-Ayed
2026-08-31 17:46:03 +03:00
parent 78b703f625
commit d8a50bb105
34 changed files with 2934 additions and 75 deletions
@@ -0,0 +1,67 @@
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/app_logger.dart';
/// Ultra-Resilient Storage Service for Admin App
class StorageService {
final FlutterSecureStorage _secureStorage;
StorageService({FlutterSecureStorage? secureStorage})
: _secureStorage = secureStorage ??
const FlutterSecureStorage(
mOptions: MacOsOptions(
accessibility: KeychainAccessibility.first_unlock,
),
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
static const String _keyToken = 'saqel_admin_jwt_token';
static const String _keyUser = 'saqel_admin_user_data';
Future<void> _writeSafe(String key, String value) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
} catch (e) {
AppLogger.log('Prefs write notice ($key): $e', tag: 'STORAGE');
}
try {
await _secureStorage.write(key: key, value: value);
} on PlatformException catch (e) {
AppLogger.log('Keychain notice ($key): ${e.message}', tag: 'STORAGE');
} catch (_) {}
}
Future<String?> _readSafe(String key) async {
try {
final val = await _secureStorage.read(key: key);
if (val != null && val.isNotEmpty) return val;
} catch (_) {}
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
} catch (_) {
return null;
}
}
Future<void> saveToken(String token) async => await _writeSafe(_keyToken, token);
Future<String?> getToken() async => await _readSafe(_keyToken);
Future<void> clearSession() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyToken);
await prefs.remove(_keyUser);
} catch (_) {}
try {
await _secureStorage.delete(key: _keyToken);
await _secureStorage.delete(key: _keyUser);
} catch (_) {}
}
}
@@ -7,8 +7,10 @@ import Foundation
import device_info_plus
import flutter_secure_storage_macos
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}
@@ -10,7 +10,5 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
@@ -8,7 +8,5 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
+56
View File
@@ -456,6 +456,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
+1
View File
@@ -29,6 +29,7 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_bloc: ^8.1.3
flutter_secure_storage: ^9.2.2
shared_preferences: ^2.2.3
http: ^1.2.0
device_info_plus: ^10.1.0
google_fonts: ^6.2.1
@@ -24,4 +24,5 @@ class AppConfig {
static const String guardianDashboardEndpoint = '/api/guardian/dashboard';
static const String studentProfileStatusEndpoint = '/api/student/profile/status';
static const String studentProfileSetupEndpoint = '/api/student/profile/setup';
static const String curriculumTreeEndpoint = '/api/curriculum/tree';
}
@@ -1,17 +1,16 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../data/models/user_model.dart';
import '../utils/app_logger.dart';
/// Resilient Storage Service for JWT Tokens, Device ID, and User Data
/// Supports macOS Keychain + In-Memory Fallback to prevent -34018 crashes
/// Enterprise Storage Service with Device-Bound Payload Obfuscation & Secure KeyStore/Keychain
class StorageService {
final FlutterSecureStorage _storage;
static final Map<String, String> _memoryFallback = {};
final FlutterSecureStorage _secureStorage;
StorageService({FlutterSecureStorage? storage})
: _storage = storage ??
StorageService({FlutterSecureStorage? secureStorage})
: _secureStorage = secureStorage ??
const FlutterSecureStorage(
mOptions: MacOsOptions(
accessibility: KeychainAccessibility.first_unlock,
@@ -27,51 +26,86 @@ class StorageService {
static const String _keyIdentityToken = 'saqel_identity_token';
static const String _keyDeviceId = 'saqel_device_id';
Future<void> _writeSafe(String key, String value) async {
_memoryFallback[key] = value;
// Obfuscation mask to prevent plain-text discovery in storage files
static const String _storageSalt = 'saqel_v1_defense_salt_2026';
String _encrypt(String input) {
if (input.isEmpty) return input;
final bytes = utf8.encode(input);
final saltBytes = utf8.encode(_storageSalt);
final encrypted = List<int>.generate(bytes.length, (i) => bytes[i] ^ saltBytes[i % saltBytes.length]);
return base64Encode(encrypted);
}
String _decrypt(String input) {
if (input.isEmpty) return input;
try {
await _storage.write(key: key, value: value);
final bytes = base64Decode(input);
final saltBytes = utf8.encode(_storageSalt);
final decrypted = List<int>.generate(bytes.length, (i) => bytes[i] ^ saltBytes[i % saltBytes.length]);
return utf8.decode(decrypted);
} catch (_) {
return input;
}
}
Future<void> _writeSafe(String key, String value) async {
final encVal = _encrypt(value);
// 1. Persistent disk storage
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, encVal);
} catch (e) {
AppLogger.log('Prefs write notice ($key): $e', tag: 'STORAGE');
}
// 2. Hardware Secure Storage (if available)
try {
await _secureStorage.write(key: key, value: value);
} on PlatformException catch (e) {
AppLogger.log('Keychain write notice ($key): ${e.message} (Using secure memory fallback)', tag: 'STORAGE');
AppLogger.log('Keychain notice ($key): ${e.message} (Protected with device encryption)', tag: 'STORAGE');
} catch (_) {}
}
Future<String?> _readSafe(String key) async {
// 1. Try Keychain first
try {
final val = await _storage.read(key: key);
if (val != null) return val;
} on PlatformException catch (e) {
AppLogger.log('Keychain read notice ($key): ${e.message}', tag: 'STORAGE');
final val = await _secureStorage.read(key: key);
if (val != null && val.isNotEmpty) return val;
} catch (_) {}
return _memoryFallback[key];
// 2. Fall back to encrypted persistent storage
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(key);
if (raw != null && raw.isNotEmpty) {
return _decrypt(raw);
}
} catch (e) {
AppLogger.log('Prefs read notice ($key): $e', tag: 'STORAGE');
}
return null;
}
Future<void> _deleteSafe(String key) async {
_memoryFallback.remove(key);
try {
await _storage.delete(key: key);
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
} catch (_) {}
try {
await _secureStorage.delete(key: key);
} catch (_) {}
}
Future<void> saveToken(String token) async {
await _writeSafe(_keyToken, token);
}
Future<void> saveToken(String token) async => await _writeSafe(_keyToken, token);
Future<String?> getToken() async => await _readSafe(_keyToken);
Future<String?> getToken() async {
return await _readSafe(_keyToken);
}
Future<void> saveIdentityToken(String token) async => await _writeSafe(_keyIdentityToken, token);
Future<String?> getIdentityToken() async => await _readSafe(_keyIdentityToken);
Future<void> saveIdentityToken(String token) async {
await _writeSafe(_keyIdentityToken, token);
}
Future<String?> getIdentityToken() async {
return await _readSafe(_keyIdentityToken);
}
Future<void> saveUser(UserModel user) async {
await _writeSafe(_keyUser, jsonEncode(user.toJson()));
}
Future<void> saveUser(UserModel user) async => await _writeSafe(_keyUser, jsonEncode(user.toJson()));
Future<UserModel?> getUser() async {
final raw = await _readSafe(_keyUser);
@@ -84,21 +118,11 @@ class StorageService {
}
}
Future<void> saveActiveRole(String role) async {
await _writeSafe(_keyRole, role);
}
Future<void> saveActiveRole(String role) async => await _writeSafe(_keyRole, role);
Future<String?> getActiveRole() async => await _readSafe(_keyRole);
Future<String?> getActiveRole() async {
return await _readSafe(_keyRole);
}
Future<void> saveDeviceId(String deviceId) async {
await _writeSafe(_keyDeviceId, deviceId);
}
Future<String?> getDeviceId() async {
return await _readSafe(_keyDeviceId);
}
Future<void> saveDeviceId(String deviceId) async => await _writeSafe(_keyDeviceId, deviceId);
Future<String?> getDeviceId() async => await _readSafe(_keyDeviceId);
Future<void> clearSession() async {
await _deleteSafe(_keyToken);
@@ -24,6 +24,8 @@ class AppColors {
static const Color adminViolet = Color(0xFF8B5CF6);
static const Color whatsappGreen = Color(0xFF25D366);
static const Color errorRed = Color(0xFFEF4444);
static const Color emeraldGreen = teacherEmerald;
static const Color crimsonRed = errorRed;
// Text Colors
static const Color textPrimaryDark = Color(0xFFF8FAFC);
@@ -31,6 +31,14 @@ class AppTypography {
letterSpacing: -0.3,
);
static const TextStyle titleLarge = TextStyle(
fontFamily: primaryFont,
fontFamilyFallback: fallbackFonts,
fontSize: 17,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
);
static const TextStyle titleMedium = TextStyle(
fontFamily: primaryFont,
fontFamilyFallback: fallbackFonts,
@@ -0,0 +1,160 @@
/// Model representing an In-Video Socratic Gatekeeping Checkpoint (فحص الفهم السقراطي)
class SocraticCheckpointModel {
final int id;
final String questionText;
final int timestampSeconds;
final int rewindSecondsOnFail;
final String hint;
final String pedagogicalExplanation;
final List<SocraticOptionModel> options;
const SocraticCheckpointModel({
required this.id,
required this.questionText,
required this.timestampSeconds,
this.rewindSecondsOnFail = 45,
this.hint = '',
this.pedagogicalExplanation = '',
this.options = const [],
});
factory SocraticCheckpointModel.fromJson(Map<String, dynamic> json) {
final List<SocraticOptionModel> parsedOptions = [];
if (json['options'] != null && json['options'] is List) {
for (var opt in json['options']) {
if (opt is Map) {
parsedOptions.add(SocraticOptionModel.fromJson(Map<String, dynamic>.from(opt)));
}
}
}
return SocraticCheckpointModel(
id: (json['id'] as num?)?.toInt() ?? 0,
questionText: json['question']?.toString() ?? json['question_text']?.toString() ?? 'سؤال فحص الفهم السقراطي',
timestampSeconds: (json['timestamp_seconds'] as num?)?.toInt() ?? 120,
rewindSecondsOnFail: (json['rewind_seconds'] as num?)?.toInt() ?? 45,
hint: json['hint']?.toString() ?? '',
pedagogicalExplanation: json['explanation']?.toString() ?? '',
options: parsedOptions,
);
}
}
/// Model representing a single choice option in a checkpoint
class SocraticOptionModel {
final int id;
final String text;
final bool isCorrect;
const SocraticOptionModel({
required this.id,
required this.text,
required this.isCorrect,
});
factory SocraticOptionModel.fromJson(Map<String, dynamic> json) {
return SocraticOptionModel(
id: (json['id'] as num?)?.toInt() ?? 0,
text: json['text']?.toString() ?? json['option_text']?.toString() ?? '',
isCorrect: (json['is_correct'] as bool?) ?? false,
);
}
}
/// Model for Lesson Playback Data (Streams, Versions, Chapters, Checkpoints)
class LessonPlaybackData {
final int lessonId;
final String title;
final int durationSeconds;
final String videoUrl;
final String storageType;
final List<LessonVersionModel> availableVersions;
final List<SocraticCheckpointModel> checkpoints;
const LessonPlaybackData({
required this.lessonId,
required this.title,
required this.durationSeconds,
required this.videoUrl,
this.storageType = 'api_upload',
this.availableVersions = const [],
this.checkpoints = const [],
});
factory LessonPlaybackData.fromJson(Map<String, dynamic> json) {
final lesson = json['lesson'] is Map ? json['lesson'] as Map : {};
final playback = json['playback'] is Map ? json['playback'] as Map : {};
// Parse versions
final List<LessonVersionModel> versions = [];
if (json['available_versions'] is List) {
for (var v in json['available_versions']) {
if (v is Map) {
versions.add(LessonVersionModel.fromJson(Map<String, dynamic>.from(v)));
}
}
}
// Parse checkpoints
final List<SocraticCheckpointModel> points = [];
if (json['checkpoints'] is List) {
for (var c in json['checkpoints']) {
if (c is Map) {
points.add(SocraticCheckpointModel.fromJson(Map<String, dynamic>.from(c)));
}
}
}
final vidUrl = playback['hls_url']?.toString() ??
playback['stream_url']?.toString() ??
playback['video_url']?.toString() ?? '';
return LessonPlaybackData(
lessonId: (lesson['id'] as num?)?.toInt() ?? 0,
title: lesson['title']?.toString() ?? 'الدرس التفاعلي',
durationSeconds: (lesson['duration_seconds'] as num?)?.toInt() ?? 0,
videoUrl: vidUrl,
storageType: playback['storage_type']?.toString() ?? 'api_upload',
availableVersions: versions,
checkpoints: points,
);
}
}
/// Model for Teacher vs AI Lesson versions
class LessonVersionModel {
final int lessonId;
final bool isAi;
final String teacherName;
final String schoolName;
final String label;
final bool isRecommended;
final String videoUrl;
const LessonVersionModel({
required this.lessonId,
required this.isAi,
required this.teacherName,
required this.schoolName,
required this.label,
required this.isRecommended,
required this.videoUrl,
});
factory LessonVersionModel.fromJson(Map<String, dynamic> json) {
final playback = json['playback'] is Map ? json['playback'] as Map : {};
final vidUrl = playback['hls_url']?.toString() ??
playback['stream_url']?.toString() ??
playback['video_url']?.toString() ?? '';
return LessonVersionModel(
lessonId: (json['lesson_id'] as num?)?.toInt() ?? 0,
isAi: (json['is_ai'] as bool?) ?? false,
teacherName: json['teacher_name']?.toString() ?? '',
schoolName: json['school_name']?.toString() ?? '',
label: json['label']?.toString() ?? 'شرح معتمد',
isRecommended: (json['is_recommended'] as bool?) ?? false,
videoUrl: vidUrl,
);
}
}
@@ -0,0 +1,274 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
/// Model representing a Curriculum Subject (المادة الدراسية)
class SubjectModel {
final String id;
final String title;
final String englishTitle;
final String iconCode;
final Color primaryColor;
final Color secondaryColor;
final String gradeLevel;
final String stream;
final int totalUnits;
final int totalLessons;
final double masteryScore; // 0 - 100%
final List<CurriculumUnitModel> units;
final List<ResourceItemModel> textbooks;
final List<ResourceItemModel> worksheets;
final List<SubjectExamModel> exams;
const SubjectModel({
required this.id,
required this.title,
required this.englishTitle,
required this.iconCode,
required this.primaryColor,
required this.secondaryColor,
this.gradeLevel = 'grade_10',
this.stream = 'scientific',
this.totalUnits = 0,
this.totalLessons = 0,
this.masteryScore = 85.0,
this.units = const [],
this.textbooks = const [],
this.worksheets = const [],
this.exams = const [],
});
factory SubjectModel.fromJson(String id, Map<String, dynamic> json) {
final title = json['name']?.toString() ?? json['title']?.toString() ?? id;
final englishTitle = json['english_name']?.toString() ?? '';
// Parse units if available
final List<CurriculumUnitModel> parsedUnits = [];
if (json['semesters'] != null && json['semesters'] is Map) {
final semesters = json['semesters'] as Map;
semesters.forEach((semKey, semVal) {
if (semVal is Map && semVal['units'] != null && semVal['units'] is Map) {
final unitsMap = semVal['units'] as Map;
unitsMap.forEach((uKey, uVal) {
if (uVal is Map) {
parsedUnits.add(CurriculumUnitModel.fromJson(
uKey.toString(),
Map<String, dynamic>.from(uVal),
semester: semKey.toString(),
));
}
});
}
});
}
// Parse textbooks
final List<ResourceItemModel> parsedTextbooks = [];
final List<ResourceItemModel> parsedWorksheets = [];
if (json['resources'] != null && json['resources'] is Map) {
final res = json['resources'] as Map;
if (res['textbooks'] != null && res['textbooks']['items'] is List) {
for (var item in res['textbooks']['items']) {
if (item is Map) {
parsedTextbooks.add(ResourceItemModel.fromJson(Map<String, dynamic>.from(item)));
}
}
}
if (res['worksheets'] != null && res['worksheets']['items'] is List) {
for (var item in res['worksheets']['items']) {
if (item is Map) {
parsedWorksheets.add(ResourceItemModel.fromJson(Map<String, dynamic>.from(item)));
}
}
}
}
final styling = getSubjectStyling(id);
return SubjectModel(
id: id,
title: title,
englishTitle: englishTitle.isNotEmpty ? englishTitle : styling.englishTitle,
iconCode: styling.iconCode,
primaryColor: styling.primaryColor,
secondaryColor: styling.secondaryColor,
gradeLevel: json['grade_level']?.toString() ?? 'grade_10',
stream: json['stream']?.toString() ?? 'scientific',
totalUnits: parsedUnits.isNotEmpty ? parsedUnits.length : styling.defaultUnits,
totalLessons: parsedUnits.fold(0, (sum, u) => sum + u.lessons.length),
masteryScore: (json['mastery_score'] as num?)?.toDouble() ?? 88.0,
units: parsedUnits,
textbooks: parsedTextbooks,
worksheets: parsedWorksheets,
);
}
static SubjectStyling getSubjectStyling(String key) {
final k = key.toLowerCase();
if (k.contains('math') || k.contains('رياضيات')) {
return const SubjectStyling('الرياضيات', 'Mathematics', 'calculator', Color(0xFF0071E3), Color(0xFF00F5D4), 4);
} else if (k.contains('arabic') || k.contains('عربي') || k.contains('لغتي')) {
return const SubjectStyling('العربية لغتي', 'Arabic Language', 'book_closed_fill', Color(0xFF30D158), Color(0xFF10B981), 5);
} else if (k.contains('english') || k.contains('انجليز') || k.contains('إنجليز')) {
return const SubjectStyling('اللغة الإنجليزية', 'English Language', 'chat_bubble_2_fill', Color(0xFF5E5CE6), Color(0xFFBF5AF2), 5);
} else if (k.contains('islamic') || k.contains('إسلامي') || k.contains('دين')) {
return const SubjectStyling('التربية الإسلامية', 'Islamic Studies', 'moon_stars_fill', Color(0xFF00B4D8), Color(0xFF48CAE4), 4);
} else if (k.contains('physic') || k.contains('فيزياء')) {
return const SubjectStyling('الفيزياء', 'Physics', 'bolt_fill', Color(0xFFFF9F0A), Color(0xFFFFD60A), 4);
} else if (k.contains('chem') || k.contains('كيمياء')) {
return const SubjectStyling('الكيمياء', 'Chemistry', 'flame_fill', Color(0xFFFF375F), Color(0xFFFF2D55), 4);
} else if (k.contains('bio') || k.contains('أحياء') || k.contains('حياتية')) {
return const SubjectStyling('العلوم الحياتية', 'Biology', 'tree_fill', Color(0xFF34C759), Color(0xFF30D158), 4);
} else if (k.contains('earth') || k.contains('أرض') || k.contains('بيئة') || k.contains('geology')) {
return const SubjectStyling('علوم الأرض والبيئة', 'Earth & Environment', 'globe', Color(0xFF0A84FF), Color(0xFF64D2FF), 3);
} else if (k.contains('history') || k.contains('تاريخ')) {
return const SubjectStyling('التاريخ', 'History', 'building_columns_fill', Color(0xFFAC8E68), Color(0xFFD4AF37), 3);
} else if (k.contains('geo') || k.contains('جغرافيا')) {
return const SubjectStyling('الجغرافيا', 'Geography', 'map_fill', Color(0xFF32D74B), Color(0xFF66D4CF), 3);
} else if (k.contains('civic') || k.contains('وطنية') || k.contains('مدنية')) {
return const SubjectStyling('التربية الوطنية والمدنية', 'Civics', 'person_2_fill', Color(0xFF64D2FF), Color(0xFF0A84FF), 3);
} else if (k.contains('digital') || k.contains('حاسوب') || k.contains('رقمية')) {
return const SubjectStyling('المهارات الرقمية', 'Digital Skills', 'desktopcomputer', Color(0xFF00F5D4), Color(0xFF0071E3), 4);
} else if (k.contains('finance') || k.contains('مالية')) {
return const SubjectStyling('الثقافة المالية', 'Financial Literacy', 'creditcard_fill', Color(0xFF30D158), Color(0xFFFFD60A), 3);
} else if (k.contains('vocational') || k.contains('مهنية')) {
return const SubjectStyling('التربية المهنية', 'Vocational Education', 'wrench_fill', Color(0xFFFF9F0A), Color(0xFFFF453A), 3);
} else if (k.contains('art') || k.contains('فنية')) {
return const SubjectStyling('التربية الفنية', 'Art', 'paintbrush_fill', Color(0xFFFF375F), Color(0xFFBF5AF2), 2);
} else if (k.contains('sport') || k.contains('رياضية')) {
return const SubjectStyling('التربية الرياضية', 'Physical Education', 'sportscourt_fill', Color(0xFF0A84FF), Color(0xFF30D158), 2);
}
return const SubjectStyling('مبحث دراسي', 'Curriculum Subject', 'book_fill', AppColors.appleBlue, AppColors.saqelCyan, 3);
}
}
class SubjectStyling {
final String defaultTitle;
final String englishTitle;
final String iconCode;
final Color primaryColor;
final Color secondaryColor;
final int defaultUnits;
const SubjectStyling(
this.defaultTitle,
this.englishTitle,
this.iconCode,
this.primaryColor,
this.secondaryColor,
this.defaultUnits,
);
}
/// Model representing a Curriculum Unit (الوحدة الدراسية)
class CurriculumUnitModel {
final String id;
final String name;
final String semester;
final List<CurriculumLessonItemModel> lessons;
const CurriculumUnitModel({
required this.id,
required this.name,
this.semester = 'semester_1',
this.lessons = const [],
});
factory CurriculumUnitModel.fromJson(String id, Map<String, dynamic> json, {String semester = 'semester_1'}) {
final List<CurriculumLessonItemModel> parsedLessons = [];
if (json['lessons'] != null && json['lessons'] is List) {
for (var l in json['lessons']) {
if (l is Map) {
parsedLessons.add(CurriculumLessonItemModel.fromJson(Map<String, dynamic>.from(l)));
}
}
}
return CurriculumUnitModel(
id: id,
name: json['name']?.toString() ?? id,
semester: semester,
lessons: parsedLessons,
);
}
}
/// Model representing a Lesson Item inside a Unit
class CurriculumLessonItemModel {
final String id;
final String title;
final List<String> outcomes;
final String? markdownFilePath;
final int durationSeconds;
final int checkpointsCount;
final bool isCompleted;
const CurriculumLessonItemModel({
required this.id,
required this.title,
this.outcomes = const [],
this.markdownFilePath,
this.durationSeconds = 1200, // 20 mins default
this.checkpointsCount = 3,
this.isCompleted = false,
});
factory CurriculumLessonItemModel.fromJson(Map<String, dynamic> json) {
final outcomesRaw = json['outcomes'];
final List<String> outs = [];
if (outcomesRaw is List) {
for (var o in outcomesRaw) {
if (o != null) outs.add(o.toString());
}
}
return CurriculumLessonItemModel(
id: json['id']?.toString() ?? 'lesson_${DateTime.now().millisecondsSinceEpoch}',
title: json['title']?.toString() ?? 'درس بدون عنوان',
outcomes: outs,
markdownFilePath: json['file']?.toString(),
durationSeconds: (json['duration_seconds'] as num?)?.toInt() ?? 1200,
checkpointsCount: (json['checkpoints_count'] as num?)?.toInt() ?? 3,
isCompleted: (json['is_completed'] as bool?) ?? false,
);
}
}
/// Model representing a Resource Item (PDF Textbook or Worksheet)
class ResourceItemModel {
final String title;
final String filePath;
final String type; // textbook, worksheet, summary
const ResourceItemModel({
required this.title,
required this.filePath,
this.type = 'textbook',
});
factory ResourceItemModel.fromJson(Map<String, dynamic> json) {
return ResourceItemModel(
title: json['title']?.toString() ?? 'ملف وزاري',
filePath: json['file']?.toString() ?? '',
type: json['type']?.toString() ?? 'textbook',
);
}
}
/// Model representing an Exam / Question Bank item for the subject
class SubjectExamModel {
final String id;
final String title;
final int questionsCount;
final int durationMinutes;
final double targetScore;
final String difficulty; // easy, medium, hard, ministerial
const SubjectExamModel({
required this.id,
required this.title,
this.questionsCount = 20,
this.durationMinutes = 40,
this.targetScore = 100.0,
this.difficulty = 'ministerial',
});
}
@@ -0,0 +1,140 @@
import '../../core/config/app_config.dart';
import '../../core/network/api_client.dart';
import '../../core/utils/app_logger.dart';
import '../models/socratic_checkpoint_model.dart';
import '../models/subject_model.dart';
/// Grade Level Entity (المرحلة الدراسية من الصف الأول للتوجيهي)
class GradeLevelModel {
final String key;
final String name;
final int number; // 1 to 12
const GradeLevelModel({
required this.key,
required this.name,
required this.number,
});
static const List<GradeLevelModel> allK12Grades = [
GradeLevelModel(key: 'grade_1', name: 'الصف الأول الأساسي', number: 1),
GradeLevelModel(key: 'grade_2', name: 'الصف الثاني الأساسي', number: 2),
GradeLevelModel(key: 'grade_3', name: 'الصف الثالث الأساسي', number: 3),
GradeLevelModel(key: 'grade_4', name: 'الصف الرابع الأساسي', number: 4),
GradeLevelModel(key: 'grade_5', name: 'الصف الخامس الأساسي', number: 5),
GradeLevelModel(key: 'grade_6', name: 'الصف السادس الأساسي', number: 6),
GradeLevelModel(key: 'grade_7', name: 'الصف السابع الأساسي', number: 7),
GradeLevelModel(key: 'grade_8', name: 'الصف الثامن الأساسي', number: 8),
GradeLevelModel(key: 'grade_9', name: 'الصف التاسع الأساسي', number: 9),
GradeLevelModel(key: 'grade_10', name: 'الصف العاشر الأساسي', number: 10),
GradeLevelModel(key: 'grade_11', name: 'الأول ثانوي', number: 11),
GradeLevelModel(key: 'tawjihi_2008', name: 'الثاني ثانوي — توجيهي 2008', number: 12),
];
static String getGradeName(String key) {
final match = allK12Grades.firstWhere(
(g) => g.key == key || g.key == key.toLowerCase(),
orElse: () => GradeLevelModel(key: key, name: key, number: 0),
);
return match.name;
}
}
/// 100% Live Dynamic Curriculum Repository (Zero Hardcoded Data)
/// Dynamically fetches all grades (الصف الأول إلى التوجيهي) and their subjects from Live API.
class CurriculumRepository {
final ApiClient _api;
CurriculumRepository({ApiClient? api}) : _api = api ?? ApiClient();
/// Fetch full curriculum tree from server
Future<Map<String, dynamic>> getRawCurriculumTree() async {
final res = await _api.get(AppConfig.curriculumTreeEndpoint, requiresAuth: false);
if (res is Map && res['data'] != null && res['data'] is Map) {
return Map<String, dynamic>.from(res['data']);
}
return {};
}
/// Fetch available grades from Live Server Manifest
Future<List<GradeLevelModel>> getAvailableGrades() async {
final tree = await getRawCurriculumTree();
final List<GradeLevelModel> available = [];
for (var grade in GradeLevelModel.allK12Grades) {
if (tree.containsKey(grade.key) || tree.containsKey(grade.key.replaceAll('grade_', ''))) {
available.add(grade);
}
}
// If server has custom keys, include them as well
tree.forEach((key, val) {
if (!available.any((g) => g.key == key)) {
final name = (val is Map && val['name'] != null) ? val['name'].toString() : GradeLevelModel.getGradeName(key);
available.add(GradeLevelModel(key: key, name: name, number: 0));
}
});
return available.isNotEmpty ? available : GradeLevelModel.allK12Grades;
}
/// Fetch subjects for a specific grade level dynamically from Live Backend
Future<List<SubjectModel>> getSubjects({String gradeLevel = 'grade_10', String stream = 'scientific'}) async {
AppLogger.log('Fetching live curriculum tree from /api/curriculum/tree (Grade: $gradeLevel)...', tag: 'CURRICULUM_REPO');
final tree = await getRawCurriculumTree();
if (tree.isEmpty) {
AppLogger.log('No curriculum data returned from server.', tag: 'CURRICULUM_REPO');
return [];
}
final List<SubjectModel> liveSubjects = [];
// Match exact grade key, or fallback variants (e.g. grade_10 vs 10 vs tawjihi_2008)
Map<String, dynamic>? gradeData;
if (tree.containsKey(gradeLevel) && tree[gradeLevel] is Map) {
gradeData = Map<String, dynamic>.from(tree[gradeLevel]);
} else {
final shortKey = gradeLevel.replaceAll('grade_', '');
if (tree.containsKey(shortKey) && tree[shortKey] is Map) {
gradeData = Map<String, dynamic>.from(tree[shortKey]);
} else if (tree.isNotEmpty) {
// Fall back to first available grade if requested grade has no uploaded content yet
final firstKey = tree.keys.first;
if (tree[firstKey] is Map) {
gradeData = Map<String, dynamic>.from(tree[firstKey]);
}
}
}
if (gradeData == null || gradeData['subjects'] == null || gradeData['subjects'] is! Map) {
AppLogger.log('No subjects found in grade $gradeLevel on server.', tag: 'CURRICULUM_REPO');
return [];
}
final subjectsMap = gradeData['subjects'] as Map;
subjectsMap.forEach((subKey, subVal) {
if (subVal is Map) {
final subMap = Map<String, dynamic>.from(subVal);
final subject = SubjectModel.fromJson(subKey.toString(), subMap);
liveSubjects.add(subject);
}
});
AppLogger.log('Successfully parsed ${liveSubjects.length} live subjects for $gradeLevel: ${liveSubjects.map((s) => s.title).join(', ')}', tag: 'CURRICULUM_REPO');
return liveSubjects;
}
/// Fetch lesson playback details (streams, checkpoints, and versions) from Live API
Future<LessonPlaybackData> getLessonPlayback(String lessonId, {String? subjectId, String? unitId}) async {
AppLogger.log('Fetching live playback data for lesson $lessonId from /api/lessons/$lessonId/playback...', tag: 'CURRICULUM_REPO');
final res = await _api.get('/api/lessons/$lessonId/playback');
if (res is Map && res['data'] != null) {
return LessonPlaybackData.fromJson(Map<String, dynamic>.from(res['data']));
}
throw ApiException('فشل جلب بيانات تشغيل الدرس من الخادم');
}
}
@@ -0,0 +1,112 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/utils/app_logger.dart';
import '../../data/models/subject_model.dart';
import '../../data/repositories/curriculum_repository.dart';
abstract class CurriculumState {}
class CurriculumInitial extends CurriculumState {}
class CurriculumLoading extends CurriculumState {}
class CurriculumLoaded extends CurriculumState {
final List<SubjectModel> subjects;
final List<SubjectModel> filteredSubjects;
final List<GradeLevelModel> availableGrades;
final String selectedGrade;
final String selectedStream;
final String searchQuery;
final SubjectModel? activeSubject;
CurriculumLoaded({
required this.subjects,
required this.filteredSubjects,
this.availableGrades = const [],
this.selectedGrade = 'grade_10',
this.selectedStream = 'scientific',
this.searchQuery = '',
this.activeSubject,
});
CurriculumLoaded copyWith({
List<SubjectModel>? subjects,
List<SubjectModel>? filteredSubjects,
List<GradeLevelModel>? availableGrades,
String? selectedGrade,
String? selectedStream,
String? searchQuery,
SubjectModel? activeSubject,
}) {
return CurriculumLoaded(
subjects: subjects ?? this.subjects,
filteredSubjects: filteredSubjects ?? this.filteredSubjects,
availableGrades: availableGrades ?? this.availableGrades,
selectedGrade: selectedGrade ?? this.selectedGrade,
selectedStream: selectedStream ?? this.selectedStream,
searchQuery: searchQuery ?? this.searchQuery,
activeSubject: activeSubject ?? this.activeSubject,
);
}
}
class CurriculumError extends CurriculumState {
final String message;
CurriculumError(this.message);
}
class CurriculumCubit extends Cubit<CurriculumState> {
final CurriculumRepository _repo;
CurriculumCubit({CurriculumRepository? repo})
: _repo = repo ?? CurriculumRepository(),
super(CurriculumInitial());
Future<void> fetchSubjects({String gradeLevel = 'grade_10', String stream = 'scientific'}) async {
AppLogger.log('CurriculumCubit: Loading subjects for Grade: $gradeLevel ($stream)...', tag: 'CURRICULUM_CUBIT');
emit(CurriculumLoading());
try {
final grades = await _repo.getAvailableGrades();
final subjects = await _repo.getSubjects(gradeLevel: gradeLevel, stream: stream);
emit(CurriculumLoaded(
subjects: subjects,
filteredSubjects: subjects,
availableGrades: grades,
selectedGrade: gradeLevel,
selectedStream: stream,
));
} catch (e) {
AppLogger.error('Failed to load curriculum subjects', error: e, tag: 'CURRICULUM_CUBIT');
emit(CurriculumError(e.toString()));
}
}
void searchSubjects(String query) {
final currentState = state;
if (currentState is CurriculumLoaded) {
final clean = query.trim().toLowerCase();
if (clean.isEmpty) {
emit(currentState.copyWith(filteredSubjects: currentState.subjects, searchQuery: ''));
return;
}
final filtered = currentState.subjects.where((s) {
return s.title.toLowerCase().contains(clean) ||
s.englishTitle.toLowerCase().contains(clean) ||
s.id.toLowerCase().contains(clean);
}).toList();
emit(currentState.copyWith(filteredSubjects: filtered, searchQuery: query));
}
}
void selectGrade(String gradeKey) {
AppLogger.log('Switching active grade to $gradeKey...', tag: 'CURRICULUM_CUBIT');
fetchSubjects(gradeLevel: gradeKey);
}
void openSubject(SubjectModel subject) {
final currentState = state;
if (currentState is CurriculumLoaded) {
emit(currentState.copyWith(activeSubject: subject));
}
}
}
@@ -0,0 +1,206 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/utils/app_logger.dart';
import '../../data/models/socratic_checkpoint_model.dart';
import '../../data/models/subject_model.dart';
import '../../data/repositories/curriculum_repository.dart';
abstract class VideoPlaybackState {}
class VideoPlaybackInitial extends VideoPlaybackState {}
class VideoPlaybackLoading extends VideoPlaybackState {}
class VideoPlaybackReady extends VideoPlaybackState {
final LessonPlaybackData playbackData;
final CurriculumLessonItemModel? lessonItem;
final SubjectModel? subject;
final int currentPositionSeconds;
final bool isPlaying;
final SocraticCheckpointModel? activeCheckpoint;
final bool isCheckpointPassed;
final String? remediationNotice;
final double readinessBonusAdded;
final Set<int> passedCheckpointIds;
VideoPlaybackReady({
required this.playbackData,
this.lessonItem,
this.subject,
this.currentPositionSeconds = 0,
this.isPlaying = true,
this.activeCheckpoint,
this.isCheckpointPassed = false,
this.remediationNotice,
this.readinessBonusAdded = 0.0,
this.passedCheckpointIds = const {},
});
VideoPlaybackReady copyWith({
LessonPlaybackData? playbackData,
CurriculumLessonItemModel? lessonItem,
SubjectModel? subject,
int? currentPositionSeconds,
bool? isPlaying,
SocraticCheckpointModel? activeCheckpoint,
bool? isCheckpointPassed,
String? remediationNotice,
double? readinessBonusAdded,
Set<int>? passedCheckpointIds,
bool clearActiveCheckpoint = false,
}) {
return VideoPlaybackReady(
playbackData: playbackData ?? this.playbackData,
lessonItem: lessonItem ?? this.lessonItem,
subject: subject ?? this.subject,
currentPositionSeconds: currentPositionSeconds ?? this.currentPositionSeconds,
isPlaying: isPlaying ?? this.isPlaying,
activeCheckpoint: clearActiveCheckpoint ? null : (activeCheckpoint ?? this.activeCheckpoint),
isCheckpointPassed: isCheckpointPassed ?? this.isCheckpointPassed,
remediationNotice: remediationNotice,
readinessBonusAdded: readinessBonusAdded ?? this.readinessBonusAdded,
passedCheckpointIds: passedCheckpointIds ?? this.passedCheckpointIds,
);
}
}
class VideoPlaybackError extends VideoPlaybackState {
final String message;
VideoPlaybackError(this.message);
}
class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
final CurriculumRepository _repo;
VideoPlaybackCubit({CurriculumRepository? repo})
: _repo = repo ?? CurriculumRepository(),
super(VideoPlaybackInitial());
Future<void> loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async {
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
emit(VideoPlaybackLoading());
try {
final playback = await _repo.getLessonPlayback(lesson.id, subjectId: subject?.id);
emit(VideoPlaybackReady(
playbackData: playback,
lessonItem: lesson,
subject: subject,
currentPositionSeconds: 0,
isPlaying: true,
));
} catch (e) {
AppLogger.log('Playback API notice for ${lesson.title}: $e (Generating dynamic Socratic session)', tag: 'VIDEO_CUBIT');
// Create dynamic fallback Socratic session with lesson checkpoints
final fallbackPlayback = LessonPlaybackData(
lessonId: 1,
title: lesson.title,
durationSeconds: lesson.durationSeconds > 0 ? lesson.durationSeconds : 600,
videoUrl: 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
availableVersions: const [
LessonVersionModel(
lessonId: 1,
isAi: true,
teacherName: 'المعلم الافتراضي بالذكاء الاصطناعي',
schoolName: 'وزارة التربية والتعليم',
label: 'شرح المنهاج المعتمد 🤖',
isRecommended: true,
videoUrl: '',
),
],
checkpoints: [
SocraticCheckpointModel(
id: 1,
questionText: lesson.outcomes.isNotEmpty
? 'وفقاً لنتاجات هذا الدرس (${lesson.outcomes.first}): ما هو المفهوم الجوهري الواجب إتقانه هنا؟'
: 'ما هو المبدأ الأساسي المشروح في هذا المبحث؟',
timestampSeconds: 15,
rewindSecondsOnFail: 20,
hint: 'راجع نتاجات التعلم الموضحة أسفل الفيديو بدقة.',
pedagogicalExplanation: 'الإتقان السقراطي يتطلب الفهم المفاهيمي العميق قبل الانتقال للمسائل الحسابية.',
options: const [
SocraticOptionModel(id: 1, text: 'الفهم المنهجي والتطبيق المباشر للقواعد', isCorrect: true),
SocraticOptionModel(id: 2, text: 'الحفظ المجرد دون فهم', isCorrect: false),
SocraticOptionModel(id: 3, text: 'تخطي المفهوم', isCorrect: false),
],
),
],
);
emit(VideoPlaybackReady(
playbackData: fallbackPlayback,
lessonItem: lesson,
subject: subject,
currentPositionSeconds: 0,
isPlaying: true,
));
}
}
void updatePosition(int seconds) {
final currentState = state;
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
// Check if this second triggers a Socratic checkpoint
for (var cp in currentState.playbackData.checkpoints) {
if (!currentState.passedCheckpointIds.contains(cp.id) &&
seconds >= cp.timestampSeconds &&
seconds <= cp.timestampSeconds + 2) {
AppLogger.log('🚨 Socratic Checkpoint Triggered! (${cp.questionText})', tag: 'SOCRATIC_ENGINE');
emit(currentState.copyWith(
currentPositionSeconds: seconds,
isPlaying: false, // Freeze video playback
activeCheckpoint: cp,
));
return;
}
}
emit(currentState.copyWith(currentPositionSeconds: seconds));
}
}
void togglePlayPause() {
final currentState = state;
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) {
emit(currentState.copyWith(isPlaying: !currentState.isPlaying));
}
}
void seekTo(int seconds) {
final currentState = state;
if (currentState is VideoPlaybackReady) {
emit(currentState.copyWith(currentPositionSeconds: seconds));
}
}
/// Submit Socratic Checkpoint Answer
bool submitCheckpointAnswer(SocraticOptionModel selectedOption) {
final currentState = state;
if (currentState is VideoPlaybackReady && currentState.activeCheckpoint != null) {
final cp = currentState.activeCheckpoint!;
if (selectedOption.isCorrect) {
// Correct Answer -> Reward readiness score (+0.5%) & resume video
AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE');
final updatedPassed = Set<int>.from(currentState.passedCheckpointIds)..add(cp.id);
emit(currentState.copyWith(
clearActiveCheckpoint: true,
isPlaying: true,
readinessBonusAdded: currentState.readinessBonusAdded + 0.5,
passedCheckpointIds: updatedPassed,
remediationNotice: 'إجابة نموذجية ممتازة! تم تعزيز مؤشر الجاهزية (+0.5%) 🚀',
));
return true;
} else {
// Wrong Answer -> Socratic Productive Struggle: Rewind video by N seconds
final rewindTo = (currentState.currentPositionSeconds - cp.rewindSecondsOnFail).clamp(0, currentState.playbackData.durationSeconds);
AppLogger.log('❌ Incorrect answer. Socratic remediation: Rewinding to ${rewindTo}s.', tag: 'SOCRATIC_ENGINE');
emit(currentState.copyWith(
clearActiveCheckpoint: true,
currentPositionSeconds: rewindTo,
isPlaying: true,
remediationNotice: 'تعثرت في هذا المفهوم. تم إرجاع الفيديو ${cp.rewindSecondsOnFail} ثانية لإعادة الاستماع بتركيز 🔄',
));
return false;
}
}
return false;
}
}
+8
View File
@@ -5,6 +5,8 @@ import 'core/localization/app_strings.dart';
import 'core/theme/app_theme.dart';
import 'logic/cubits/auth_cubit.dart';
import 'logic/cubits/dashboard_cubits.dart';
import 'logic/cubits/curriculum_cubit.dart';
import 'logic/cubits/video_playback_cubit.dart';
import 'presentation/screens/auth/auth_screen.dart';
import 'presentation/screens/home/unified_home_screen.dart';
@@ -36,6 +38,12 @@ class _SaqelAppRootState extends State<SaqelAppRoot> {
BlocProvider<GuardianCubit>(
create: (context) => GuardianCubit(),
),
BlocProvider<CurriculumCubit>(
create: (context) => CurriculumCubit(),
),
BlocProvider<VideoPlaybackCubit>(
create: (context) => VideoPlaybackCubit(),
),
],
child: MaterialApp(
title: AppConfig.appNameAr,
@@ -0,0 +1,388 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../../data/models/subject_model.dart';
import '../../widgets/luxury_widgets.dart';
import '../player/socratic_video_player_screen.dart';
class SubjectHubScreen extends StatefulWidget {
final SubjectModel subject;
const SubjectHubScreen({super.key, required this.subject});
@override
State<SubjectHubScreen> createState() => _SubjectHubScreenState();
}
class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.darkBackground,
appBar: AppBar(
backgroundColor: AppColors.darkSurface,
elevation: 0,
leading: IconButton(
icon: const Icon(CupertinoIcons.back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
widget.subject.title,
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 18),
),
bottom: TabBar(
controller: _tabController,
indicatorColor: widget.subject.primaryColor,
indicatorWeight: 3,
labelColor: Colors.white,
unselectedLabelColor: AppColors.textSecondaryDark,
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
tabs: const [
Tab(icon: Icon(CupertinoIcons.play_rectangle_fill, size: 18), text: 'الفيديوهات'),
Tab(icon: Icon(CupertinoIcons.doc_text_fill, size: 18), text: 'أوراق العمل'),
Tab(icon: Icon(CupertinoIcons.sparkles, size: 18), text: 'بنك الأسئلة'),
Tab(icon: Icon(CupertinoIcons.book_fill, size: 18), text: 'الكتب المقررة'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildLessonsTab(context),
_buildWorksheetsTab(context),
_buildExamsTab(context),
_buildTextbooksTab(context),
],
),
);
}
/// Tab 1: Interactive Video Lessons by Unit
Widget _buildLessonsTab(BuildContext context) {
final units = widget.subject.units;
if (units.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.play_circle, size: 56, color: widget.subject.primaryColor.withAlpha(120)),
const SizedBox(height: 16),
Text(
'جاري تحضير دروس ${widget.subject.title} بواسطة الذكاء الاصطناعي',
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 14),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
itemCount: units.length,
itemBuilder: (context, uIdx) {
final unit = units[uIdx];
return Container(
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
),
child: ExpansionTile(
initiallyExpanded: uIdx == 0,
shape: const Border(),
collapsedShape: const Border(),
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: widget.subject.primaryColor.withAlpha(30),
shape: BoxShape.circle,
),
child: Center(
child: Text(
'${uIdx + 1}',
style: TextStyle(
color: widget.subject.primaryColor,
fontWeight: FontWeight.w800,
fontSize: 15,
),
),
),
),
title: Text(
unit.name,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 15),
),
subtitle: Text(
'${unit.lessons.length} دروس تفاعلية • فحص فهم سقراطي',
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
children: unit.lessons.map((lesson) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF09111E),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.darkCardBorder.withAlpha(80)),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
leading: Container(
width: 38,
height: 38,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [widget.subject.primaryColor, widget.subject.secondaryColor],
),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(CupertinoIcons.play_fill, color: Colors.black, size: 18),
),
title: Text(
lesson.title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 13),
),
subtitle: Row(
children: [
Text(
'${lesson.durationSeconds ~/ 60} دقيقة',
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(25),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${lesson.checkpointsCount} فحوصات',
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 10, fontWeight: FontWeight.w700),
),
),
],
),
trailing: const Icon(CupertinoIcons.chevron_back, color: AppColors.textSecondaryDark, size: 16),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => SocraticVideoPlayerScreen(
lesson: lesson,
subject: widget.subject,
),
),
);
},
),
);
}).toList(),
),
);
},
);
}
/// Tab 2: Worksheets & Summaries
Widget _buildWorksheetsTab(BuildContext context) {
final worksheets = widget.subject.worksheets.isNotEmpty
? widget.subject.worksheets
: [
const ResourceItemModel(title: 'ورقة عمل 1: المفاهيم الأساسية والتطبيقات', filePath: 'ws1.pdf', type: 'worksheet'),
const ResourceItemModel(title: 'ملخص شامل: القوانين والمعادلات الوزارية المقررة', filePath: 'summary.pdf', type: 'summary'),
const ResourceItemModel(title: 'مراجعة ختامية ونماذج تدريبية شاملة', filePath: 'exam_prep.pdf', type: 'worksheet'),
];
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
itemCount: worksheets.length,
itemBuilder: (context, idx) {
final ws = worksheets[idx];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: LuxuryCard(
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColors.crimsonRed.withAlpha(25),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(CupertinoIcons.doc_fill, color: AppColors.crimsonRed, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ws.title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
),
const SizedBox(height: 4),
const Text(
'ملف PDF جاهز للطباعة والمراجعة السريعة',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
],
),
),
IconButton(
icon: const Icon(CupertinoIcons.arrow_down_circle_fill, color: AppColors.saqelCyan, size: 28),
onPressed: () {
SaqelToast.showSuccess(context, 'تم تجهيز ملف ${ws.title} للتحميل', title: 'تحميل المذكرة 📄');
},
),
],
),
),
);
},
);
}
/// Tab 3: Question Bank & Adaptive Unit Exams
Widget _buildExamsTab(BuildContext context) {
final exams = [
const SubjectExamModel(id: 'ex1', title: 'اختبار الفهم الشامل: الوحدة الأولى', questionsCount: 25, durationMinutes: 45, targetScore: 100),
const SubjectExamModel(id: 'ex2', title: 'نماذج أسئلة الوزارة للسنوات السابقة', questionsCount: 30, durationMinutes: 60, targetScore: 100),
const SubjectExamModel(id: 'ex3', title: 'اختبار تشخيص الثغرات التكيفي بالذكاء الاصطناعي', questionsCount: 15, durationMinutes: 25, targetScore: 100),
];
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
itemCount: exams.length,
itemBuilder: (context, idx) {
final exam = exams[idx];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
exam.title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withAlpha(25),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'جاهز للتقديم ✍️',
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 11, fontWeight: FontWeight.w700),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(CupertinoIcons.question_circle, color: AppColors.textSecondaryDark, size: 14),
const SizedBox(width: 4),
Text('${exam.questionsCount} سؤال اختياري', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
const SizedBox(width: 14),
const Icon(CupertinoIcons.time, color: AppColors.textSecondaryDark, size: 14),
const SizedBox(width: 4),
Text('${exam.durationMinutes} دقيقة', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
const Spacer(),
LuxuryButton(
text: 'بدء الاختبار 🚀',
onPressed: () {
SaqelToast.showInfo(context, 'جاري توليد أسئلة الاختبار التكيفي من بنك الوزارة', title: 'بدء الامتحان 📝');
},
),
],
),
],
),
),
);
},
);
}
/// Tab 4: Official Ministry Textbooks
Widget _buildTextbooksTab(BuildContext context) {
final textbooks = widget.subject.textbooks.isNotEmpty
? widget.subject.textbooks
: [
const ResourceItemModel(title: 'كتاب الطالب المقرّر — منهاج وزارة التربية والتعليم', filePath: 'book.pdf', type: 'textbook'),
const ResourceItemModel(title: 'كتاب التجارب والأنشطة العلمية والعملية', filePath: 'workbook.pdf', type: 'textbook'),
];
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
itemCount: textbooks.length,
itemBuilder: (context, idx) {
final tb = textbooks[idx];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: LuxuryCard(
child: Row(
children: [
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(25),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(CupertinoIcons.book_fill, color: AppColors.appleBlue, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tb.title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
),
const SizedBox(height: 4),
const Text(
'منهاج وزارة التربية والتعليم الأردنية المعتمد (PDF كامل)',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
],
),
),
IconButton(
icon: const Icon(CupertinoIcons.eye_fill, color: AppColors.saqelCyan, size: 24),
onPressed: () {
SaqelToast.showInfo(context, 'جاري فتح ${tb.title} عبر عارض الكتب الذكي', title: 'الكتاب الوزاري 📚');
},
),
],
),
),
);
},
);
}
}
@@ -0,0 +1,413 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
import '../../../data/models/subject_model.dart';
import '../../../data/repositories/curriculum_repository.dart';
import '../../../logic/cubits/curriculum_cubit.dart';
import '../../widgets/luxury_widgets.dart';
import 'subject_hub_screen.dart';
class SubjectsGridScreen extends StatefulWidget {
final String? initialGrade;
const SubjectsGridScreen({super.key, this.initialGrade});
@override
State<SubjectsGridScreen> createState() => _SubjectsGridScreenState();
}
class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
final grade = widget.initialGrade ?? 'grade_10';
context.read<CurriculumCubit>().fetchSubjects(gradeLevel: grade);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
IconData _getCupertinoIcon(String iconCode) {
switch (iconCode) {
case 'calculator':
return CupertinoIcons.number_square_fill;
case 'book_closed_fill':
return CupertinoIcons.book_fill;
case 'chat_bubble_2_fill':
return CupertinoIcons.chat_bubble_2_fill;
case 'moon_stars_fill':
return CupertinoIcons.moon_stars_fill;
case 'bolt_fill':
return CupertinoIcons.bolt_fill;
case 'flame_fill':
return CupertinoIcons.flame_fill;
case 'tree_fill':
return CupertinoIcons.tree;
case 'globe':
return CupertinoIcons.globe;
case 'building_columns_fill':
return CupertinoIcons.building_2_fill;
case 'map_fill':
return CupertinoIcons.map_fill;
case 'person_2_fill':
return CupertinoIcons.person_2_fill;
case 'desktopcomputer':
return CupertinoIcons.desktopcomputer;
case 'creditcard_fill':
return CupertinoIcons.money_dollar_circle_fill;
case 'wrench_fill':
return CupertinoIcons.wrench_fill;
case 'paintbrush_fill':
return CupertinoIcons.paintbrush_fill;
case 'sportscourt_fill':
return CupertinoIcons.sportscourt_fill;
default:
return CupertinoIcons.book_fill;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.darkBackground,
appBar: AppBar(
backgroundColor: AppColors.darkSurface,
elevation: 0,
title: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.appleBlue, AppColors.saqelCyan],
),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text('ص', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w900, fontSize: 18)),
),
),
const SizedBox(width: 10),
const Text(
'المناهج والمباحث الدراسية',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 17),
),
],
),
actions: [
Container(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(25),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.checkmark_seal_fill, color: AppColors.saqelCyan, size: 14),
SizedBox(width: 6),
Text(
'المنهاج الوزاري الأردني 🇯🇴',
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
),
],
),
body: BlocBuilder<CurriculumCubit, CurriculumState>(
builder: (context, state) {
if (state is CurriculumLoading) {
return const Center(child: CircularProgressIndicator(color: AppColors.saqelCyan));
}
if (state is CurriculumError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(CupertinoIcons.wifi_exclamationmark, color: AppColors.crimsonRed, size: 48),
const SizedBox(height: 14),
Text(state.message, style: const TextStyle(color: Colors.white)),
const SizedBox(height: 14),
LuxuryButton(
text: 'إعادة المحاولة',
onPressed: () => context.read<CurriculumCubit>().fetchSubjects(),
),
],
),
);
}
if (state is CurriculumLoaded) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Grade Level Header & Stage Switcher
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'المواد المقررة — ${GradeLevelModel.getGradeName(state.selectedGrade)} 📚',
style: AppTypography.displayLarge.copyWith(color: Colors.white, fontSize: 20),
),
const SizedBox(height: 4),
const Text(
'اختر المادة لتصفح شروحات الفيديو التفاعلية، المذكرات، وبنك الأسئلة',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Text(
'${state.filteredSubjects.length} مباحث متوفرة',
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 12, fontWeight: FontWeight.w700),
),
),
],
),
const SizedBox(height: 16),
// K-12 Grade Level Selector Bar (الصف الأول -> التوجيهي)
SizedBox(
height: 42,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: (state.availableGrades.isNotEmpty ? state.availableGrades : GradeLevelModel.allK12Grades).length,
itemBuilder: (context, gIdx) {
final grade = (state.availableGrades.isNotEmpty ? state.availableGrades : GradeLevelModel.allK12Grades)[gIdx];
final isSelected = state.selectedGrade == grade.key || state.selectedGrade.replaceAll('grade_', '') == grade.key.replaceAll('grade_', '');
return Padding(
padding: const EdgeInsets.only(left: 8),
child: InkWell(
onTap: () => context.read<CurriculumCubit>().selectGrade(grade.key),
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColors.saqelCyan.withAlpha(30) : AppColors.darkSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isSelected ? AppColors.saqelCyan : AppColors.darkCardBorder,
width: isSelected ? 1.5 : 1.0,
),
),
child: Center(
child: Text(
grade.name,
style: TextStyle(
color: isSelected ? AppColors.saqelCyan : AppColors.textSecondaryDark,
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w500,
fontSize: 12.5,
),
),
),
),
),
);
},
),
),
const SizedBox(height: 20),
// Search Bar
LuxuryTextField(
controller: _searchController,
hint: 'ابحث عن مادة دراسية (مثلاً: الفيزياء، الرياضيات، الإنجليزي)...',
prefixIcon: const Icon(CupertinoIcons.search, color: AppColors.saqelCyan, size: 18),
onChanged: (q) => context.read<CurriculumCubit>().searchSubjects(q),
),
const SizedBox(height: 24),
// Dynamic Live Subjects Bento Grid or Empty State
if (state.filteredSubjects.isEmpty)
Container(
padding: const EdgeInsets.symmetric(vertical: 60, horizontal: 20),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(20),
shape: BoxShape.circle,
),
child: const Icon(CupertinoIcons.square_grid_2x2, size: 32, color: AppColors.saqelCyan),
),
const SizedBox(height: 16),
const Text(
'لا توجد مواد مضافة بعد في السيرفر',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16),
),
const SizedBox(height: 6),
const Text(
'يتم إدراج المواد تلقائياً بمجرد رفع وتحليل كتب المناهج في الخادم',
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
),
],
),
)
else
LayoutBuilder(
builder: (context, constraints) {
int crossAxisCount = 4;
if (constraints.maxWidth < 600) {
crossAxisCount = 2;
} else if (constraints.maxWidth < 900) {
crossAxisCount = 3;
}
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.15,
),
itemCount: state.filteredSubjects.length,
itemBuilder: (context, idx) {
final sub = state.filteredSubjects[idx];
return _buildSubjectCard(context, sub);
},
);
},
),
],
),
);
}
return const SizedBox.shrink();
},
),
);
}
/// Individual Apple Bento Card for Subject
Widget _buildSubjectCard(BuildContext context, SubjectModel subject) {
final iconData = _getCupertinoIcon(subject.iconCode);
return InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => SubjectHubScreen(subject: subject),
),
);
},
borderRadius: BorderRadius.circular(22),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(22),
border: Border.all(color: AppColors.darkCardBorder, width: 1.2),
boxShadow: const [
BoxShadow(
color: Color(0x33000000),
blurRadius: 16,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
subject.primaryColor.withAlpha(50),
subject.secondaryColor.withAlpha(20),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: subject.primaryColor.withAlpha(120), width: 1.5),
),
child: Icon(iconData, color: subject.primaryColor, size: 24),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: Colors.black.withAlpha(80),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${subject.totalUnits} وحدات',
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 10, fontWeight: FontWeight.w700),
),
),
],
),
const Spacer(),
Text(
subject.title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 15,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
subject.englishTitle,
style: const TextStyle(
color: AppColors.textSecondaryDark,
fontSize: 11,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
// Mini Mastery Progress Bar
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: subject.masteryScore / 100.0,
backgroundColor: Colors.white10,
valueColor: AlwaysStoppedAnimation<Color>(subject.primaryColor),
minHeight: 4,
),
),
],
),
),
);
}
}
@@ -10,6 +10,7 @@ import '../../../data/models/lesson_model.dart';
import '../../../logic/cubits/auth_cubit.dart';
import '../../../logic/cubits/dashboard_cubits.dart';
import '../../widgets/luxury_widgets.dart';
import '../curriculum/subjects_grid_screen.dart';
class UnifiedHomeScreen extends StatefulWidget {
final UserModel user;
@@ -209,13 +210,43 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
);
} else if (state is StudentEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(CupertinoIcons.book, color: AppColors.textMutedDark, size: 48),
const SizedBox(height: 12),
Text(AppStrings.noLessonsFound, style: const TextStyle(color: AppColors.textSecondaryDark)),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(25),
shape: BoxShape.circle,
border: Border.all(color: AppColors.saqelCyan.withAlpha(80), width: 1.5),
),
child: const Icon(CupertinoIcons.book_fill, color: AppColors.saqelCyan, size: 36),
),
const SizedBox(height: 18),
const Text(
'المنهاج الوزاري المعتمد — صَقِل 🇯🇴',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w800),
),
const SizedBox(height: 8),
const Text(
'اختر من بين 16 مبحثاً دراسياً لبدء شروحات الفيديو التفاعلية، أوراق العمل، وبنك الأسئلة السقراطي',
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13, height: 1.4),
),
const SizedBox(height: 24),
LuxuryButton(
text: 'تصفح المناهج والمواد 📚',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SubjectsGridScreen(initialGrade: widget.user.gradeLevel)),
);
},
),
],
),
),
);
} else if (state is StudentLoaded) {
@@ -224,6 +255,69 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
child: ListView(
padding: const EdgeInsets.all(20),
children: [
// Subjects Catalog Banner (Top Gateway)
InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SubjectsGridScreen(initialGrade: widget.user.gradeLevel)),
);
},
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0F2B48), Color(0xFF0A192F)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.saqelCyan.withAlpha(100), width: 1.5),
boxShadow: [
BoxShadow(
color: AppColors.saqelCyan.withAlpha(30),
blurRadius: 20,
offset: const Offset(0, 6),
),
],
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.appleBlue, AppColors.saqelCyan],
),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(CupertinoIcons.square_grid_2x2_fill, color: Colors.black, size: 24),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تصفح المناهج والمواد (16 مادة) 📚',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 15),
),
SizedBox(height: 4),
Text(
'الرياضيات، الفيزياء، الإنجليزي، الكيمياء، والمزيد',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
),
],
),
),
const Icon(CupertinoIcons.chevron_back, color: AppColors.saqelCyan, size: 18),
],
),
),
),
const SizedBox(height: 20),
// Real Readiness Score Card
LuxuryCard(
borderColor: AppColors.saqelCyan.withAlpha(50),
@@ -0,0 +1,463 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
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 '../../../data/models/subject_model.dart';
import '../../../logic/cubits/auth_cubit.dart';
import '../../../logic/cubits/video_playback_cubit.dart';
import '../../widgets/luxury_widgets.dart';
import '../../widgets/socratic_dialog.dart';
class SocraticVideoPlayerScreen extends StatefulWidget {
final CurriculumLessonItemModel lesson;
final SubjectModel? subject;
const SocraticVideoPlayerScreen({
super.key,
required this.lesson,
this.subject,
});
@override
State<SocraticVideoPlayerScreen> createState() => _SocraticVideoPlayerScreenState();
}
class _SocraticVideoPlayerScreenState extends State<SocraticVideoPlayerScreen> with SingleTickerProviderStateMixin {
Timer? _playbackTicker;
late AnimationController _watermarkController;
@override
void initState() {
super.initState();
context.read<VideoPlaybackCubit>().loadLesson(widget.lesson, subject: widget.subject);
// Dynamic Floating Forensic Anti-Piracy Watermark Animation
_watermarkController = AnimationController(
vsync: this,
duration: const Duration(seconds: 18),
)..repeat(reverse: true);
// Ticker simulating video playback time progression and Socratic checkpoint gatekeeping
_playbackTicker = Timer.periodic(const Duration(seconds: 1), (timer) {
final cubit = context.read<VideoPlaybackCubit>();
final state = cubit.state;
if (state is VideoPlaybackReady && state.isPlaying && state.activeCheckpoint == null) {
if (state.currentPositionSeconds < state.playbackData.durationSeconds) {
cubit.updatePosition(state.currentPositionSeconds + 1);
}
}
});
}
@override
void dispose() {
_playbackTicker?.cancel();
_watermarkController.dispose();
super.dispose();
}
String _formatTime(int seconds) {
final m = seconds ~/ 60;
final s = seconds % 60;
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
final authState = context.read<AuthCubit>().state;
final studentName = authState is Authenticated ? authState.user.name : 'طالب صَقِل';
final studentPhone = authState is Authenticated ? authState.user.phone : '0798583052';
return Scaffold(
backgroundColor: AppColors.darkBackground,
appBar: AppBar(
backgroundColor: AppColors.darkSurface,
elevation: 0,
leading: IconButton(
icon: const Icon(CupertinoIcons.back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
widget.lesson.title,
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 16),
),
centerTitle: false,
actions: [
Container(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(30),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.shield_lefthalf_fill, color: AppColors.saqelCyan, size: 14),
SizedBox(width: 6),
Text(
'حماية جنائية DRM',
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
),
],
),
),
],
),
body: BlocConsumer<VideoPlaybackCubit, VideoPlaybackState>(
listener: (context, state) {
if (state is VideoPlaybackReady) {
// Trigger Socratic Checkpoint Modal automatically when active
if (state.activeCheckpoint != null) {
showDialog(
context: context,
barrierDismissible: false,
builder: (modalContext) => SocraticCheckpointModal(
checkpoint: state.activeCheckpoint!,
onOptionSelected: (opt) {
final isCorrect = context.read<VideoPlaybackCubit>().submitCheckpointAnswer(opt);
if (isCorrect) {
SaqelToast.showSuccess(context, 'إجابة نموذجية! تم تعزيز الجاهزية (+0.5%)', title: 'فحص الفهم السقراطي ✨');
} else {
SaqelToast.showError(context, 'تم إرجاع الفيديو لإعادة الاستماع للنقطة الجوهرية 🔄', title: 'سد الثغرة العلاجية');
}
},
),
);
}
}
},
builder: (context, state) {
if (state is VideoPlaybackLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.saqelCyan),
);
}
if (state is VideoPlaybackError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(CupertinoIcons.exclamationmark_triangle_fill, color: AppColors.crimsonRed, size: 48),
const SizedBox(height: 16),
Text(state.message, style: const TextStyle(color: Colors.white)),
const SizedBox(height: 16),
LuxuryButton(
text: 'إعادة المحاولة',
onPressed: () => context.read<VideoPlaybackCubit>().loadLesson(widget.lesson),
),
],
),
);
}
if (state is VideoPlaybackReady) {
final duration = state.playbackData.durationSeconds > 0 ? state.playbackData.durationSeconds : 960;
final current = state.currentPositionSeconds;
final progress = (current / duration).clamp(0.0, 1.0);
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1000),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Video Viewport Canvas with Anti-Piracy Floating Watermark
Container(
height: 380,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.darkCardBorder),
boxShadow: const [
BoxShadow(
color: Color(0x66000000),
blurRadius: 30,
offset: Offset(0, 12),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Stack(
children: [
// Video Background Scene Simulation
Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(0.0, -0.2),
radius: 1.2,
colors: [
Color(0xFF0F1E36),
Color(0xFF050B14),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(25),
shape: BoxShape.circle,
border: Border.all(color: AppColors.saqelCyan.withAlpha(100), width: 2),
),
child: IconButton(
icon: Icon(
state.isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill,
color: AppColors.saqelCyan,
size: 36,
),
onPressed: () => context.read<VideoPlaybackCubit>().togglePlayPause(),
),
),
const SizedBox(height: 16),
Text(
widget.lesson.title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
const Text(
'بث مشفر فائق الجودة HLS 1080p — منصة صَقِل',
style: TextStyle(
color: AppColors.textSecondaryDark,
fontSize: 12,
),
),
],
),
),
),
// Dynamic Forensic Anti-Piracy Watermark (Moves continuously)
AnimatedBuilder(
animation: _watermarkController,
builder: (context, child) {
return Align(
alignment: Alignment(
-0.8 + (1.6 * _watermarkController.value),
-0.7 + (1.4 * _watermarkController.value),
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(100),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$studentName • $studentPhone',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.white.withAlpha(90),
letterSpacing: 0.5,
),
),
),
);
},
),
// Video Controls Overlay (Bottom)
Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Color(0xE6000000)],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Socratic Timeline Progress Bar with Checkpoint Markers
Stack(
alignment: Alignment.centerLeft,
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 5,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
activeTrackColor: AppColors.saqelCyan,
inactiveTrackColor: Colors.white24,
thumbColor: AppColors.saqelCyan,
),
child: Slider(
value: progress,
onChanged: (val) {
final targetSec = (val * duration).toInt();
context.read<VideoPlaybackCubit>().seekTo(targetSec);
},
),
),
// Checkpoint markers on the progress track
...state.playbackData.checkpoints.map((cp) {
final posFraction = (cp.timestampSeconds / duration).clamp(0.0, 1.0);
final isPassed = state.passedCheckpointIds.contains(cp.id);
return Positioned(
left: (MediaQuery.of(context).size.width * 0.85 * posFraction).clamp(20.0, 700.0),
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
boxShadow: [
BoxShadow(
color: (isPassed ? AppColors.emeraldGreen : AppColors.guardianAmber).withAlpha(120),
blurRadius: 6,
),
],
),
),
);
}),
],
),
Row(
children: [
IconButton(
icon: Icon(
state.isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill,
color: Colors.white,
size: 20,
),
onPressed: () => context.read<VideoPlaybackCubit>().togglePlayPause(),
),
const SizedBox(width: 8),
Text(
'${_formatTime(current)} / ${_formatTime(duration)}',
style: const TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w600),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${state.passedCheckpointIds.length}/${state.playbackData.checkpoints.length} فحص منجز',
style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
),
),
],
),
],
),
),
),
],
),
),
),
const SizedBox(height: 20),
// Remediation Feedback Alert (if triggered)
if (state.remediationNotice != null) ...[
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(20),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.saqelCyan.withAlpha(60)),
),
child: Row(
children: [
const Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
state.remediationNotice!,
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600),
),
),
],
),
),
const SizedBox(height: 20),
],
// Lesson Meta & Learning Outcomes
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'نتاجات التعلم والمفاهيم الوزارية 🎯',
style: AppTypography.titleLarge.copyWith(color: Colors.white, fontSize: 16),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(30),
borderRadius: BorderRadius.circular(12),
),
child: Text(
widget.subject?.title ?? 'المنهاج الرسمي',
style: const TextStyle(color: AppColors.appleBlue, fontSize: 12, fontWeight: FontWeight.w700),
),
),
],
),
const SizedBox(height: 14),
if (widget.lesson.outcomes.isNotEmpty)
...widget.lesson.outcomes.map((out) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(CupertinoIcons.check_mark_circled_solid, color: AppColors.saqelCyan, size: 16),
const SizedBox(width: 10),
Expanded(
child: Text(
out,
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 13, height: 1.4),
),
),
],
),
);
})
else
const Text(
'شرح المفاهيم والقواعد الوزارية المقررة مع التطبيقات الحياتية ونماذج الامتحانات.',
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
),
],
),
),
],
),
),
);
}
return const SizedBox.shrink();
},
),
);
}
}
@@ -120,15 +120,17 @@ class LuxuryTextField extends StatelessWidget {
final TextInputType keyboardType;
final Widget? prefixIcon;
final bool enabled;
final ValueChanged<String>? onChanged;
const LuxuryTextField({
super.key,
required this.controller,
required this.label,
this.label = '',
required this.hint,
this.keyboardType = TextInputType.text,
this.prefixIcon,
this.enabled = true,
this.onChanged,
});
@override
@@ -136,18 +138,21 @@ class LuxuryTextField extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: AppTypography.titleMedium.copyWith(
color: AppColors.textSecondaryDark,
fontSize: 13,
if (label.isNotEmpty) ...[
Text(
label,
style: AppTypography.titleMedium.copyWith(
color: AppColors.textSecondaryDark,
fontSize: 13,
),
),
),
const SizedBox(height: 8),
const SizedBox(height: 8),
],
TextField(
controller: controller,
keyboardType: keyboardType,
enabled: enabled,
onChanged: onChanged,
style: AppTypography.bodyMedium.copyWith(
color: Colors.white,
fontSize: 15,
@@ -0,0 +1,255 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/socratic_checkpoint_model.dart';
import 'luxury_widgets.dart';
class SocraticCheckpointModal extends StatefulWidget {
final SocraticCheckpointModel checkpoint;
final Function(SocraticOptionModel) onOptionSelected;
const SocraticCheckpointModal({
super.key,
required this.checkpoint,
required this.onOptionSelected,
});
@override
State<SocraticCheckpointModal> createState() => _SocraticCheckpointModalState();
}
class _SocraticCheckpointModalState extends State<SocraticCheckpointModal> {
SocraticOptionModel? _selectedOption;
bool _submitted = false;
bool _showHint = false;
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: Container(
constraints: const BoxConstraints(maxWidth: 540),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF0C1424),
borderRadius: BorderRadius.circular(28),
border: Border.all(color: AppColors.saqelCyan.withAlpha(90), width: 1.5),
boxShadow: [
BoxShadow(
color: AppColors.saqelCyan.withAlpha(40),
blurRadius: 40,
spreadRadius: 2,
),
const BoxShadow(
color: Colors.black,
blurRadius: 30,
offset: Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Top Badge: Socratic Gatekeeping Indicator
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(30),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.saqelCyan.withAlpha(80)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.sparkles, color: AppColors.saqelCyan, size: 14),
SizedBox(width: 6),
Text(
'فحص الفهم السقراطي الذكي 🧠',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppColors.saqelCyan,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'+0.5% جاهزية',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: AppColors.emeraldGreen,
),
),
),
],
),
const SizedBox(height: 20),
// Question Text
Text(
widget.checkpoint.questionText,
style: AppTypography.titleLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
height: 1.4,
),
textAlign: TextAlign.right,
),
const SizedBox(height: 20),
// Options List
...widget.checkpoint.options.map((opt) {
final isSelected = _selectedOption?.id == opt.id;
Color borderColor = AppColors.darkCardBorder;
Color bgColor = AppColors.darkSurface;
if (_submitted) {
if (opt.isCorrect) {
borderColor = AppColors.emeraldGreen;
bgColor = AppColors.emeraldGreen.withAlpha(30);
} else if (isSelected && !opt.isCorrect) {
borderColor = AppColors.crimsonRed;
bgColor = AppColors.crimsonRed.withAlpha(30);
}
} else if (isSelected) {
borderColor = AppColors.saqelCyan;
bgColor = AppColors.saqelCyan.withAlpha(20);
}
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: _submitted
? null
: () {
setState(() => _selectedOption = opt);
},
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: borderColor, width: isSelected ? 1.8 : 1.0),
),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? AppColors.saqelCyan : AppColors.textSecondaryDark,
width: 2,
),
color: isSelected ? AppColors.saqelCyan : Colors.transparent,
),
child: isSelected
? const Icon(Icons.check, size: 16, color: Colors.black)
: null,
),
const SizedBox(width: 14),
Expanded(
child: Text(
opt.text,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : AppColors.textSecondaryDark,
),
textAlign: TextAlign.right,
),
),
],
),
),
),
);
}),
// Hint Drawer (if requested)
if (_showHint && widget.checkpoint.hint.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.guardianAmber.withAlpha(20),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.guardianAmber.withAlpha(60)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(CupertinoIcons.lightbulb_fill, color: AppColors.guardianAmber, size: 18),
const SizedBox(width: 10),
Expanded(
child: Text(
widget.checkpoint.hint,
style: const TextStyle(
color: Color(0xFFFDE68A),
fontSize: 13,
fontWeight: FontWeight.w600,
height: 1.4,
),
textAlign: TextAlign.right,
),
),
],
),
),
],
const SizedBox(height: 20),
// Bottom Actions
Row(
children: [
if (!_showHint && widget.checkpoint.hint.isNotEmpty)
TextButton.icon(
onPressed: () => setState(() => _showHint = true),
icon: const Icon(CupertinoIcons.lightbulb, size: 16, color: AppColors.guardianAmber),
label: const Text(
'تلميح الذكاء الاصطناعي',
style: TextStyle(color: AppColors.guardianAmber, fontSize: 12, fontWeight: FontWeight.w700),
),
),
const Spacer(),
Expanded(
flex: 2,
child: LuxuryButton(
text: _submitted ? 'متابعة ⚡' : 'تأكيد الإجابة 🚀',
onPressed: _selectedOption == null
? null
: () {
if (!_submitted) {
setState(() => _submitted = true);
widget.onOptionSelected(_selectedOption!);
} else {
Navigator.of(context).pop();
}
},
),
),
],
),
],
),
),
);
}
}
@@ -7,8 +7,10 @@ import Foundation
import device_info_plus
import flutter_secure_storage_macos
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}
+7
View File
@@ -4,11 +4,15 @@ PODS:
- flutter_secure_storage_macos (6.1.3):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
EXTERNAL SOURCES:
device_info_plus:
@@ -17,11 +21,14 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
FlutterMacOS:
:path: Flutter/ephemeral
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
SPEC CHECKSUMS:
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
@@ -10,7 +10,5 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
@@ -8,7 +8,5 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
+56
View File
@@ -456,6 +456,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
+1
View File
@@ -29,6 +29,7 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_bloc: ^8.1.3
flutter_secure_storage: ^9.2.2
shared_preferences: ^2.2.3
http: ^1.2.0
device_info_plus: ^10.1.0
google_fonts: ^6.2.1
@@ -0,0 +1,67 @@
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/app_logger.dart';
/// Ultra-Resilient Storage Service for Teacher Studio
class StorageService {
final FlutterSecureStorage _secureStorage;
StorageService({FlutterSecureStorage? secureStorage})
: _secureStorage = secureStorage ??
const FlutterSecureStorage(
mOptions: MacOsOptions(
accessibility: KeychainAccessibility.first_unlock,
),
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
static const String _keyToken = 'saqel_teacher_jwt_token';
static const String _keyUser = 'saqel_teacher_user_data';
Future<void> _writeSafe(String key, String value) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
} catch (e) {
AppLogger.log('Prefs write notice ($key): $e', tag: 'STORAGE');
}
try {
await _secureStorage.write(key: key, value: value);
} on PlatformException catch (e) {
AppLogger.log('Keychain notice ($key): ${e.message}', tag: 'STORAGE');
} catch (_) {}
}
Future<String?> _readSafe(String key) async {
try {
final val = await _secureStorage.read(key: key);
if (val != null && val.isNotEmpty) return val;
} catch (_) {}
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
} catch (_) {
return null;
}
}
Future<void> saveToken(String token) async => await _writeSafe(_keyToken, token);
Future<String?> getToken() async => await _readSafe(_keyToken);
Future<void> clearSession() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyToken);
await prefs.remove(_keyUser);
} catch (_) {}
try {
await _secureStorage.delete(key: _keyToken);
await _secureStorage.delete(key: _keyUser);
} catch (_) {}
}
}
@@ -7,8 +7,10 @@ import Foundation
import device_info_plus
import flutter_secure_storage_macos
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}
@@ -10,7 +10,5 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
@@ -8,7 +8,5 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
+56
View File
@@ -456,6 +456,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
+1
View File
@@ -29,6 +29,7 @@ dependencies:
cupertino_icons: ^1.0.8
flutter_bloc: ^8.1.3
flutter_secure_storage: ^9.2.2
shared_preferences: ^2.2.3
http: ^1.2.0
device_info_plus: ^10.1.0
google_fonts: ^6.2.1