Update Saqel Platform: 2026-08-31 01:52:23
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
/// Global Application Configuration
|
||||
class AppConfig {
|
||||
AppConfig._();
|
||||
|
||||
// Official Live Saqel API Gateway
|
||||
static const String productionBaseUrl = 'https://saqel.intaleqapp.com';
|
||||
static const String localBaseUrl = 'http://127.0.0.1:8000';
|
||||
|
||||
// Active Base URL (Default to live production API Gateway)
|
||||
static String baseUrl = productionBaseUrl;
|
||||
|
||||
static const String appNameAr = 'صَقِل';
|
||||
static const String appNameEn = 'Saqel';
|
||||
static const String appTaglineAr = 'المنصة التعليمية الشاملة للثانوية العامة';
|
||||
static const String appTaglineEn = 'Comprehensive EdTech Platform for High School';
|
||||
|
||||
// API Endpoints
|
||||
static const String otpRequestEndpoint = '/api/auth/otp/request';
|
||||
static const String otpVerifyEndpoint = '/api/auth/otp/verify';
|
||||
static const String loginNationalIdEndpoint = '/api/auth/student/login-national-id';
|
||||
static const String meEndpoint = '/api/auth/me';
|
||||
static const String logoutEndpoint = '/api/auth/logout';
|
||||
static const String studentLessonsEndpoint = '/api/student/lessons';
|
||||
static const String guardianDashboardEndpoint = '/api/guardian/dashboard';
|
||||
static const String studentProfileStatusEndpoint = '/api/student/profile/status';
|
||||
static const String studentProfileSetupEndpoint = '/api/student/profile/setup';
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/// Multi-language Localization Dictionary for Saqel Platform
|
||||
class AppStrings {
|
||||
AppStrings._();
|
||||
|
||||
static bool isArabic = true;
|
||||
|
||||
// General
|
||||
static String get appName => isArabic ? 'صَقِل' : 'Saqel';
|
||||
static String get studentPortal => isArabic ? 'منصة الطالب' : 'Student Portal';
|
||||
static String get guardianPortal => isArabic ? 'بوابة ولي الأمر' : 'Guardian Portal';
|
||||
static String get studentRole => isArabic ? 'الطالب 🎓' : 'Student 🎓';
|
||||
static String get guardianRole => isArabic ? 'ولي الأمر 👨👧👦' : 'Guardian 👨👧👦';
|
||||
static String get logout => isArabic ? 'تسجيل الخروج' : 'Logout';
|
||||
static String get retry => isArabic ? 'إعادة المحاولة' : 'Retry';
|
||||
static String get loading => isArabic ? 'جارٍ التحميل...' : 'Loading...';
|
||||
static String get emptyData => isArabic ? 'لا توجد بيانات حالياً' : 'No data available';
|
||||
|
||||
// Auth
|
||||
static String get enterPhoneTitle => isArabic ? 'تسجيل الدخول عبر الواتساب' : 'Login via WhatsApp';
|
||||
static String get enterPhoneSubtitle => isArabic
|
||||
? 'أدخل رقم هاتفك لاستلام رمز التحقق الفوري (OTP) بدون كلمات مرور'
|
||||
: 'Enter your phone number to receive instant OTP verification';
|
||||
static String get phoneNumber => isArabic ? 'رقم الهاتف (مثال: 079XXXXXXX)' : 'Phone Number';
|
||||
static String get requestOtp => isArabic ? 'إرسال رمز التحقق 📲' : 'Send OTP Code 📲';
|
||||
static String get enterOtpTitle => isArabic ? 'تأكيد رمز التحقق' : 'Verify OTP Code';
|
||||
static String get enterOtpSubtitle => isArabic
|
||||
? 'تم إرسال رمز التحقق عبر الواتساب إلى رقمك'
|
||||
: 'Verification code sent via WhatsApp to your number';
|
||||
static String get verifyOtp => isArabic ? 'تأكيد ودخول المنصة' : 'Verify & Continue';
|
||||
static String get enterNationalIdTitle => isArabic ? 'الرقم الوطني للطالب' : 'National ID';
|
||||
static String get enterNationalIdSubtitle => isArabic
|
||||
? 'يرجى إدخال الرقم الوطني الخاص بالطالب لفتح الملف الأكاديمي'
|
||||
: 'Enter the student National ID to access the academic profile';
|
||||
static String get nationalId => isArabic ? 'الرقم الوطني (10 أرقام)' : 'National ID (10 digits)';
|
||||
static String get continueBtn => isArabic ? 'متابعة' : 'Continue';
|
||||
|
||||
// Student Dashboard
|
||||
static String get tawjihiCurriculum => isArabic ? 'المنهاج الوزاري المعتمد 📚' : 'Ministry Curriculum 📚';
|
||||
static String get readinessScore => isArabic ? 'مؤشر الجاهزية للوزاري' : 'Tawjihi Readiness Score';
|
||||
static String get socraticCheckpoint => isArabic ? 'فحص سقراطي 🎯' : 'Socratic Checkpoint 🎯';
|
||||
static String get noLessonsFound => isArabic ? 'لا توجد دروس مسجلة للمرحلة الحالية' : 'No lessons recorded for this grade yet';
|
||||
|
||||
// Guardian Dashboard
|
||||
static String get linkedChildren => isArabic ? 'الأبناء المسجلون برقم الهاتف 📱' : 'Linked Children 📱';
|
||||
static String get noChildrenLinked => isArabic ? 'لا يوجد أبناء مرتبطون بهذا الرقم حالياً' : 'No children linked to this number';
|
||||
static String get whatsappReports => isArabic ? 'تقارير نبيه التلقائية' : 'Nabeh Automated Reports';
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../config/app_config.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
/// Unified Secure REST API Client with Automatic JWT, Device Fingerprint & Debug Telemetry
|
||||
class ApiClient {
|
||||
final http.Client _client;
|
||||
final StorageService _storage;
|
||||
|
||||
ApiClient({http.Client? client, StorageService? storage})
|
||||
: _client = client ?? http.Client(),
|
||||
_storage = storage ?? StorageService();
|
||||
|
||||
Future<Map<String, String>> _buildHeaders({bool requiresAuth = true}) async {
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
final deviceId = await _storage.getDeviceId();
|
||||
if (deviceId != null) {
|
||||
headers['X-Device-Fingerprint'] = deviceId;
|
||||
}
|
||||
|
||||
if (requiresAuth) {
|
||||
final token = await _storage.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
Uri _buildUri(String path, [Map<String, dynamic>? queryParams]) {
|
||||
final base = AppConfig.baseUrl.replaceAll(RegExp(r'/+$'), '');
|
||||
final cleanPath = path.startsWith('/') ? path : '/$path';
|
||||
final uriString = '$base$cleanPath';
|
||||
final uri = Uri.parse(uriString);
|
||||
if (queryParams != null && queryParams.isNotEmpty) {
|
||||
return uri.replace(queryParameters: queryParams.map((k, v) => MapEntry(k, v.toString())));
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
Future<dynamic> get(String path, {Map<String, dynamic>? queryParams, bool requiresAuth = true}) async {
|
||||
final uri = _buildUri(path, queryParams);
|
||||
final headers = await _buildHeaders(requiresAuth: requiresAuth);
|
||||
|
||||
AppLogger.request(method: 'GET', uri: uri, headers: headers);
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final response = await _client.get(uri, headers: headers);
|
||||
stopwatch.stop();
|
||||
return _handleResponse(response, method: 'GET', uri: uri, duration: stopwatch.elapsed);
|
||||
} catch (e, stack) {
|
||||
stopwatch.stop();
|
||||
AppLogger.error('GET Request Failed ($uri)', error: e, stackTrace: stack);
|
||||
throw ApiException('فشل الاتصال بالخادم: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> post(String path, {Map<String, dynamic>? body, bool requiresAuth = true}) async {
|
||||
final uri = _buildUri(path);
|
||||
final headers = await _buildHeaders(requiresAuth: requiresAuth);
|
||||
|
||||
AppLogger.request(method: 'POST', uri: uri, headers: headers, body: body);
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final response = await _client.post(
|
||||
uri,
|
||||
headers: headers,
|
||||
body: body != null ? jsonEncode(body) : null,
|
||||
);
|
||||
stopwatch.stop();
|
||||
return _handleResponse(response, method: 'POST', uri: uri, duration: stopwatch.elapsed);
|
||||
} catch (e, stack) {
|
||||
stopwatch.stop();
|
||||
AppLogger.error('POST Request Failed ($uri)', error: e, stackTrace: stack);
|
||||
throw ApiException('فشل إرسال الطلب للخادم: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _handleResponse(
|
||||
http.Response response, {
|
||||
required String method,
|
||||
required Uri uri,
|
||||
required Duration duration,
|
||||
}) {
|
||||
dynamic data;
|
||||
try {
|
||||
data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
} catch (_) {
|
||||
data = response.body;
|
||||
}
|
||||
|
||||
AppLogger.response(
|
||||
method: method,
|
||||
uri: uri,
|
||||
statusCode: response.statusCode,
|
||||
responseBody: data,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return data;
|
||||
}
|
||||
|
||||
final message = (data is Map && data['message'] != null)
|
||||
? data['message']
|
||||
: (data is Map && data['error'] != null)
|
||||
? data['error']
|
||||
: 'خطأ من الخادم (${response.statusCode})';
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
AppLogger.log('Unauthorized (401) received, clearing session.', tag: 'AUTH');
|
||||
_storage.clearSession();
|
||||
throw UnauthorizedException(message.toString());
|
||||
}
|
||||
|
||||
throw ApiException(message.toString(), statusCode: response.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
ApiException(this.message, {this.statusCode});
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class UnauthorizedException extends ApiException {
|
||||
UnauthorizedException(super.message) : super(statusCode: 401);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.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
|
||||
class StorageService {
|
||||
final FlutterSecureStorage _storage;
|
||||
static final Map<String, String> _memoryFallback = {};
|
||||
|
||||
StorageService({FlutterSecureStorage? storage})
|
||||
: _storage = storage ??
|
||||
const FlutterSecureStorage(
|
||||
mOptions: MacOsOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
);
|
||||
|
||||
static const String _keyToken = 'saqel_jwt_token';
|
||||
static const String _keyUser = 'saqel_user_data';
|
||||
static const String _keyRole = 'saqel_active_role';
|
||||
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;
|
||||
try {
|
||||
await _storage.write(key: key, value: value);
|
||||
} on PlatformException catch (e) {
|
||||
AppLogger.log('Keychain write notice ($key): ${e.message} (Using secure memory fallback)', tag: 'STORAGE');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<String?> _readSafe(String key) async {
|
||||
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');
|
||||
} catch (_) {}
|
||||
return _memoryFallback[key];
|
||||
}
|
||||
|
||||
Future<void> _deleteSafe(String key) async {
|
||||
_memoryFallback.remove(key);
|
||||
try {
|
||||
await _storage.delete(key: key);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> saveToken(String token) async {
|
||||
await _writeSafe(_keyToken, token);
|
||||
}
|
||||
|
||||
Future<String?> getToken() async {
|
||||
return await _readSafe(_keyToken);
|
||||
}
|
||||
|
||||
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<UserModel?> getUser() async {
|
||||
final raw = await _readSafe(_keyUser);
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
try {
|
||||
final json = jsonDecode(raw);
|
||||
return UserModel.fromJson(json);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveActiveRole(String role) async {
|
||||
await _writeSafe(_keyRole, role);
|
||||
}
|
||||
|
||||
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> clearSession() async {
|
||||
await _deleteSafe(_keyToken);
|
||||
await _deleteSafe(_keyUser);
|
||||
await _deleteSafe(_keyRole);
|
||||
await _deleteSafe(_keyIdentityToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Apple Luxury Palette & Brand Tokens for Saqel Platform
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
// Dark OLED Palette
|
||||
static const Color darkBackground = Color(0xFF06090F);
|
||||
static const Color darkSurface = Color(0xFF0D111A);
|
||||
static const Color darkCard = Color(0xFF131A26);
|
||||
static const Color darkCardBorder = Color(0x1FFFFFFF);
|
||||
|
||||
// Light Palette
|
||||
static const Color lightBackground = Color(0xFFF8FAFC);
|
||||
static const Color lightSurface = Color(0xFFFFFFFF);
|
||||
static const Color lightCard = Color(0xFFF1F5F9);
|
||||
static const Color lightCardBorder = Color(0x0F000000);
|
||||
|
||||
// Accent Colors
|
||||
static const Color appleBlue = Color(0xFF0071E3);
|
||||
static const Color saqelCyan = Color(0xFF00F5D4);
|
||||
static const Color guardianAmber = Color(0xFFF59E0B);
|
||||
static const Color teacherEmerald = Color(0xFF10B981);
|
||||
static const Color adminViolet = Color(0xFF8B5CF6);
|
||||
static const Color whatsappGreen = Color(0xFF25D366);
|
||||
static const Color errorRed = Color(0xFFEF4444);
|
||||
|
||||
// Text Colors
|
||||
static const Color textPrimaryDark = Color(0xFFF8FAFC);
|
||||
static const Color textSecondaryDark = Color(0xFF94A3B8);
|
||||
static const Color textMutedDark = Color(0xFF64748B);
|
||||
|
||||
static const Color textPrimaryLight = Color(0xFF0F172A);
|
||||
static const Color textSecondaryLight = Color(0xFF475569);
|
||||
static const Color textMutedLight = Color(0xFF94A3B8);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
/// Apple Official San Francisco & SF Arabic Typography Standards
|
||||
class AppTypography {
|
||||
AppTypography._();
|
||||
|
||||
static const String primaryFont = '-apple-system';
|
||||
static const List<String> fallbackFonts = [
|
||||
'SF Pro Display',
|
||||
'SF Pro Text',
|
||||
'SF Arabic',
|
||||
'Geeza Pro',
|
||||
'Cairo',
|
||||
'system-ui',
|
||||
];
|
||||
|
||||
static const TextStyle displayLarge = TextStyle(
|
||||
fontFamily: primaryFont,
|
||||
fontFamilyFallback: fallbackFonts,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5,
|
||||
);
|
||||
|
||||
static const TextStyle headlineMedium = TextStyle(
|
||||
fontFamily: primaryFont,
|
||||
fontFamilyFallback: fallbackFonts,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.3,
|
||||
);
|
||||
|
||||
static const TextStyle titleMedium = TextStyle(
|
||||
fontFamily: primaryFont,
|
||||
fontFamilyFallback: fallbackFonts,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.2,
|
||||
);
|
||||
|
||||
static const TextStyle bodyMedium = TextStyle(
|
||||
fontFamily: primaryFont,
|
||||
fontFamilyFallback: fallbackFonts,
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5,
|
||||
);
|
||||
|
||||
static const TextStyle labelSmall = TextStyle(
|
||||
fontFamily: primaryFont,
|
||||
fontFamilyFallback: fallbackFonts,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
}
|
||||
|
||||
/// Official Apple Dark & Light Theme Factory
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: AppColors.darkBackground,
|
||||
fontFamily: AppTypography.primaryFont,
|
||||
fontFamilyFallback: AppTypography.fallbackFonts,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: AppColors.appleBlue,
|
||||
secondary: AppColors.saqelCyan,
|
||||
surface: AppColors.darkSurface,
|
||||
error: AppColors.errorRed,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.darkCard,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: const BorderSide(color: AppColors.darkCardBorder),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.appleBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 15),
|
||||
textStyle: AppTypography.titleMedium.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
brightness: Brightness.light,
|
||||
scaffoldBackgroundColor: AppColors.lightBackground,
|
||||
fontFamily: AppTypography.primaryFont,
|
||||
fontFamilyFallback: AppTypography.fallbackFonts,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: AppColors.appleBlue,
|
||||
secondary: AppColors.saqelCyan,
|
||||
surface: AppColors.lightSurface,
|
||||
error: AppColors.errorRed,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.lightCard,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: const BorderSide(color: AppColors.lightCardBorder),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Enterprise Logging Utility for Saqel Platform
|
||||
/// Active in Debug mode (kDebugMode), completely silent in Production
|
||||
class AppLogger {
|
||||
AppLogger._();
|
||||
|
||||
static void log(String message, {String tag = 'SAQEL'}) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[$tag] $message');
|
||||
}
|
||||
}
|
||||
|
||||
static void request({
|
||||
required String method,
|
||||
required Uri uri,
|
||||
Map<String, String>? headers,
|
||||
dynamic body,
|
||||
}) {
|
||||
if (!kDebugMode) return;
|
||||
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('\n🌐 ─── [HTTP REQUEST] ──────────────────────────────────────────');
|
||||
buffer.writeln('➡️ Method: $method');
|
||||
buffer.writeln('🔗 URL: $uri');
|
||||
if (headers != null && headers.isNotEmpty) {
|
||||
final safeHeaders = Map<String, String>.from(headers);
|
||||
if (safeHeaders.containsKey('Authorization')) {
|
||||
final auth = safeHeaders['Authorization']!;
|
||||
safeHeaders['Authorization'] = auth.length > 20 ? '${auth.substring(0, 15)}...' : 'Bearer [REDACTED]';
|
||||
}
|
||||
buffer.writeln('📋 Headers: ${jsonEncode(safeHeaders)}');
|
||||
}
|
||||
if (body != null) {
|
||||
buffer.writeln('📦 Body: ${body is String ? body : jsonEncode(body)}');
|
||||
}
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
||||
debugPrint(buffer.toString());
|
||||
}
|
||||
|
||||
static void response({
|
||||
required String method,
|
||||
required Uri uri,
|
||||
required int statusCode,
|
||||
dynamic responseBody,
|
||||
Duration? duration,
|
||||
}) {
|
||||
if (!kDebugMode) return;
|
||||
|
||||
final isSuccess = statusCode >= 200 && statusCode < 300;
|
||||
final emoji = isSuccess ? '✅' : '⚠️';
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('\n$emoji ─── [HTTP RESPONSE $statusCode] ────────────────────────────────');
|
||||
buffer.writeln('⬅️ Method: $method');
|
||||
buffer.writeln('🔗 URL: $uri');
|
||||
if (duration != null) {
|
||||
buffer.writeln('⏱️ Time: ${duration.inMilliseconds}ms');
|
||||
}
|
||||
if (responseBody != null) {
|
||||
buffer.writeln('📦 Data: ${responseBody is String ? responseBody : jsonEncode(responseBody)}');
|
||||
}
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
||||
debugPrint(buffer.toString());
|
||||
}
|
||||
|
||||
static void error(String message, {dynamic error, StackTrace? stackTrace, String tag = 'ERROR'}) {
|
||||
if (!kDebugMode) return;
|
||||
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('\n🚨 ─── [ERROR: $tag] ──────────────────────────────────────────');
|
||||
buffer.writeln('❌ Message: $message');
|
||||
if (error != null) {
|
||||
buffer.writeln('⚠️ Error: $error');
|
||||
}
|
||||
if (stackTrace != null) {
|
||||
buffer.writeln('📍 Trace: \n$stackTrace');
|
||||
}
|
||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
||||
debugPrint(buffer.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
/// Ultra-Luxury Apple-Grade SnackBar / Toast Notification System
|
||||
class SaqelToast {
|
||||
SaqelToast._();
|
||||
|
||||
/// Show Success Toast (Emerald / Green)
|
||||
static void showSuccess(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
title: title,
|
||||
icon: CupertinoIcons.checkmark_circle_fill,
|
||||
iconColor: AppColors.teacherEmerald,
|
||||
borderColor: AppColors.teacherEmerald.withAlpha(90),
|
||||
backgroundColor: const Color(0xE60D1A14),
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Show Error Toast (Crimson / Red)
|
||||
static void showError(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
title: title,
|
||||
icon: CupertinoIcons.exclamationmark_circle_fill,
|
||||
iconColor: AppColors.errorRed,
|
||||
borderColor: AppColors.errorRed.withAlpha(90),
|
||||
backgroundColor: const Color(0xE61A0D0D),
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Show Info Toast (Apple Blue / Cyan)
|
||||
static void showInfo(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
title: title,
|
||||
icon: CupertinoIcons.info_circle_fill,
|
||||
iconColor: AppColors.appleBlue,
|
||||
borderColor: AppColors.appleBlue.withAlpha(90),
|
||||
backgroundColor: const Color(0xE60D1424),
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Show WhatsApp / Nabeh Toast (WhatsApp Green)
|
||||
static void showWhatsApp(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
title: title,
|
||||
icon: CupertinoIcons.chat_bubble_fill,
|
||||
iconColor: AppColors.whatsappGreen,
|
||||
borderColor: AppColors.whatsappGreen.withAlpha(100),
|
||||
backgroundColor: const Color(0xE6091A12),
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void _show({
|
||||
required BuildContext context,
|
||||
required String message,
|
||||
String? title,
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required Color borderColor,
|
||||
required Color backgroundColor,
|
||||
required Duration duration,
|
||||
}) {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
duration: duration,
|
||||
elevation: 0,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.only(bottom: 24, left: 16, right: 16),
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: Colors.transparent,
|
||||
content: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: borderColor, width: 1.2),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x99000000),
|
||||
blurRadius: 25,
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withAlpha(35),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 20),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null && title.isNotEmpty) ...[
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.titleMedium.copyWith(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
Text(
|
||||
message,
|
||||
style: AppTypography.bodyMedium.copyWith(
|
||||
fontSize: 12.5,
|
||||
color: const Color(0xFFE2E8F0),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user