feat: implement core architecture, design system, localization, and trip features for both driver and rider apps.

This commit is contained in:
Hamza-Ayed
2026-07-19 16:21:58 +03:00
parent 959e2fa758
commit c4dfeb4c6f
207 changed files with 14017 additions and 5567 deletions
@@ -0,0 +1,77 @@
/// استثناءات الشبكة الموحّدة — كل خطأ يتحول إلى صنف هنا.
/// docs/23 §4
class ApiException implements Exception {
ApiException(this.message, {this.statusCode, this.type});
final String message;
final int? statusCode;
final ApiExceptionType? type;
@override
String toString() => 'ApiException($statusCode): $message';
}
enum ApiExceptionType {
network,
timeout,
unauthorized,
forbidden,
notFound,
server,
unknown,
}
/// تحليل DioException إلى ApiException
ApiException fromDioError(Object error) {
final dio = error;
final type = switch (dio.runtimeType.toString()) {
'DioException' => _mapDioType(dio),
_ => ApiExceptionType.unknown,
};
final statusCode = _extractStatusCode(dio);
final message = _extractMessage(dio, statusCode);
return ApiException(message, statusCode: statusCode, type: type);
}
int? _extractStatusCode(Object error) {
try {
return (error as dynamic).response?.statusCode as int?;
} catch (_) {
return null;
}
}
String _extractMessage(Object error, int? statusCode) {
try {
final data = (error as dynamic).response?.data;
if (data is Map) {
return (data['message'] ?? data['error'] ?? 'خطأ غير معروف').toString();
}
} catch (_) {}
return switch (statusCode) {
401 => 'انتهت الجلسة',
403 => 'غير مصرح',
404 => 'غير موجود',
500 => 'خطأ في السيرفر',
_ => 'خطأ غير معروف',
};
}
ApiExceptionType _mapDioType(Object error) {
try {
final dioType = (error as dynamic).type;
return switch (dioType.toString()) {
'DioExceptionType.connectionTimeout' => ApiExceptionType.timeout,
'DioExceptionType.sendTimeout' => ApiExceptionType.timeout,
'DioExceptionType.receiveTimeout' => ApiExceptionType.timeout,
'DioExceptionType.connectionError' => ApiExceptionType.network,
'DioExceptionType.badResponse' => ApiExceptionType.server,
'DioExceptionType.cancel' => ApiExceptionType.unknown,
_ => ApiExceptionType.unknown,
};
} catch (_) {
return ApiExceptionType.unknown;
}
}
+60
View File
@@ -0,0 +1,60 @@
/// مُولَّد من المانيفست عبر سكربت N3 — لا يُحرَّر يدوياً.
/// يحتوي على أعلام الميزات `const` + إعداد المستأجر الثابت.
/// docs/22 §1.5
class BuildConfig {
static const String tenantSlug = 'siro';
static const String baseUrl = 'https://tripz-api.intaleqapp.com/api';
static const String appName = 'Tripz';
static const String appRole = 'rider';
// ألوان المستأجر (البذرة)
static const int primaryColorValue = 0xFF0E7C66;
static const int secondaryColorValue = 0xFF0E7C66;
// أعلام الميزات — const: tree-shaking يحذف الكود المطفأ في release
// docs/22 §1.5, docs/23 §5
static const bool rideClassesSet = true;
static const bool tariffEngineFull = true;
static const bool liveTrackingPro = true;
static const bool coupons = true;
static const bool walletPayment = true;
static const bool chat = true;
static const bool aiDocumentProcessing = false;
static const bool heatmap = false;
static const bool predictiveDemand = false;
static const bool geofence = false;
static const bool targetedPromo = false;
static const bool whatsappCampaigns = false;
static const bool marketingEngine = false;
static const bool dynamicPricing = false;
static const bool transit = false;
static const bool paymentGateways = false;
static const bool smsSettlement = false;
static const bool negotiator = false;
static const bool driverTiers = false;
static const bool driverAssurance = false;
}
/// اختصار وصول للاستعمال في شروط Features.xxx
abstract final class Features {
static const bool rideClassesSet = BuildConfig.rideClassesSet;
static const bool tariffEngineFull = BuildConfig.tariffEngineFull;
static const bool liveTrackingPro = BuildConfig.liveTrackingPro;
static const bool coupons = BuildConfig.coupons;
static const bool walletPayment = BuildConfig.walletPayment;
static const bool chat = BuildConfig.chat;
static const bool aiDocumentProcessing = BuildConfig.aiDocumentProcessing;
static const bool heatmap = BuildConfig.heatmap;
static const bool predictiveDemand = BuildConfig.predictiveDemand;
static const bool geofence = BuildConfig.geofence;
static const bool targetedPromo = BuildConfig.targetedPromo;
static const bool whatsappCampaigns = BuildConfig.whatsappCampaigns;
static const bool marketingEngine = BuildConfig.marketingEngine;
static const bool dynamicPricing = BuildConfig.dynamicPricing;
static const bool transit = BuildConfig.transit;
static const bool paymentGateways = BuildConfig.paymentGateways;
static const bool smsSettlement = BuildConfig.smsSettlement;
static const bool negotiator = BuildConfig.negotiator;
static const bool driverTiers = BuildConfig.driverTiers;
static const bool driverAssurance = BuildConfig.driverAssurance;
}
+6
View File
@@ -21,4 +21,10 @@ class AppConfig {
/// دور هذا التطبيق — يميّز تطبيق الراكب عن السائق عند التسجيل.
static const String appRole = 'rider';
/// مفتاح الخريطة — يُمرَّر من البيئة. لا يُكتب في الكود (docs/23 §6).
static const String mapApiKey = String.fromEnvironment(
'MAP_API_KEY',
defaultValue: '',
);
}
+88
View File
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'tripz_colors.dart';
import 'typography.dart';
/// بناء الثيم — المدخلان الوحيدان: السطوع واللغة.
/// docs/26 §2, §4
ThemeData buildTheme({required Brightness brightness, required Locale locale}) {
final bool isDark = brightness == Brightness.dark;
final seed = const Color(0xFF0E7C66); // أخضر تركوازي — بذرة المستأجر
final colorScheme = ColorScheme.fromSeed(
seedColor: seed,
brightness: brightness,
);
final textTheme = buildTextTheme(locale);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: colorScheme,
textTheme: textTheme,
extensions: [
isDark ? TripzColors.dark : TripzColors.light,
],
appBarTheme: AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
titleTextStyle: textTheme.titleLarge?.copyWith(
color: colorScheme.onSurface,
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.error),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: textTheme.bodyLarge,
hintStyle: textTheme.bodyLarge?.copyWith(color: colorScheme.onSurfaceVariant),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: textTheme.labelLarge,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
textStyle: textTheme.labelLarge,
),
),
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: colorScheme.outlineVariant),
),
margin: EdgeInsets.zero,
),
bottomSheetTheme: const BottomSheetThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
showDragHandle: true,
),
);
}
+58
View File
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
/// الرموز الثابتة — كل قيمة هنا `const` ولا تُخترَع في الشاشات.
/// docs/26 §3
abstract final class T {
// ── المسافات (مضاعفات 4) ──
static const double s4 = 4;
static const double s8 = 8;
static const double s12 = 12;
static const double s16 = 16;
static const double s24 = 24;
static const double s32 = 32;
static const double s48 = 48;
/// حشوة الصفحة الافتراضية
static const double pagePadding = s16;
// ── الزوايا ──
static const double r8 = 8; // حقول / شارات
static const double r12 = 12; // أزرار / بطاقات
static const double r16 = 16; // محتوى مدمّق
static const double r24 = 24; // bottom sheets
static const double rCircle = 999; // avatars
// ── الحركة (أزمنة) ──
static const Duration fast = Duration(milliseconds: 120); // لمسة
static const Duration medium = Duration(milliseconds: 240); // انتقال ضمن الشاشة
static const Duration slow = Duration(milliseconds: 400); // sheet / صفحة
// ── المسافات الإضافية ──
static const double s20 = 20;
// ── الظلال ──
static const BoxShadow shadowSm = BoxShadow(
color: Color(0x0F000000),
blurRadius: 4,
offset: Offset(0, 1),
);
static const BoxShadow shadowLg = BoxShadow(
color: Color(0x1A000000),
blurRadius: 12,
offset: Offset(0, 4),
);
// ── الأيقونات ──
static const double iconSm = 20;
static const double iconMd = 24;
static const double iconLg = 32;
// ── الارتفاعات ──
static const double buttonHeight = 52;
static const double inputHeight = 52;
static const double touchTarget = 48;
// ── عناصر الخريطة ──
static const double markerSize = 40;
static const double polylineWidth = 6;
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
/// الأدوار الدلالية غير الموجودة في ColorScheme الأساسي.
/// الشاشة تستدعيها عبر `context.tripzColors` — لا تقرّر درجة اللون بنفسها.
/// docs/26 §2
@immutable
class TripzColors extends ThemeExtension<TripzColors> {
const TripzColors({
required this.success,
required this.warning,
required this.danger,
required this.info,
required this.surfaceRaised,
required this.mapOverlayDark,
required this.mapOverlayLight,
});
final Color success;
final Color warning;
final Color danger;
final Color info;
final Color surfaceRaised;
final Color mapOverlayDark;
final Color mapOverlayLight;
@override
TripzColors copyWith({
Color? success,
Color? warning,
Color? danger,
Color? info,
Color? surfaceRaised,
Color? mapOverlayDark,
Color? mapOverlayLight,
}) {
return TripzColors(
success: success ?? this.success,
warning: warning ?? this.warning,
danger: danger ?? this.danger,
info: info ?? this.info,
surfaceRaised: surfaceRaised ?? this.surfaceRaised,
mapOverlayDark: mapOverlayDark ?? this.mapOverlayDark,
mapOverlayLight: mapOverlayLight ?? this.mapOverlayLight,
);
}
@override
TripzColors lerp(TripzColors? other, double t) {
if (other is! TripzColors) return this;
return TripzColors(
success: Color.lerp(success, other.success, t)!,
warning: Color.lerp(warning, other.warning, t)!,
danger: Color.lerp(danger, other.danger, t)!,
info: Color.lerp(info, other.info, t)!,
surfaceRaised: Color.lerp(surfaceRaised, other.surfaceRaised, t)!,
mapOverlayDark: Color.lerp(mapOverlayDark, other.mapOverlayDark, t)!,
mapOverlayLight: Color.lerp(mapOverlayLight, other.mapOverlayLight, t)!,
);
}
// ── الوضع النهاري ──
static const light = TripzColors(
success: Color(0xFF2E7D32),
warning: Color(0xFFF9A825),
danger: Color(0xFFD32F2F),
info: Color(0xFF1565C0),
surfaceRaised: Color(0xFFF5F5F5),
mapOverlayDark: Color(0xCC1A1A2E),
mapOverlayLight: Color(0xCCFFFFFF),
);
// ── الوضع الليلي ──
static const dark = TripzColors(
success: Color(0xFF66BB6A),
warning: Color(0xFFFFD54F),
danger: Color(0xFFEF5350),
info: Color(0xFF42A5F5),
surfaceRaised: Color(0xFF2C2C3A),
mapOverlayDark: Color(0xCC0D0D1A),
mapOverlayLight: Color(0xCC1A1A2E),
);
}
/// امتداد مريح للوصول السريع من السياق
extension TripzColorsExtension on BuildContext {
TripzColors get tripzColors =>
Theme.of(this).extension<TripzColors>() ?? TripzColors.light;
}
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
/// الخطوط المضمّنة محلياً — Inter + IBM Plex Sans Arabic.
/// لا حزمة google_fonts — الاتصال لا يُراهَن عليه.
/// docs/26 §1
///
/// الأوزان المضمّنة: Regular(400) · Medium(500) · SemiBold(600) · Bold(700)
/// لكل خط (٨ ملفات في `assets/fonts/`).
TextTheme buildTextTheme(Locale locale) {
final bool isArabic = locale.languageCode == 'ar';
// الخط الرئيسي: عربي ⇒ Plex Arabic، إنجليزي ⇒ Inter
final String primary = isArabic ? 'IBMPlexSansArabic' : 'Inter';
// الاحتياطي: العكس
final String fallback = isArabic ? 'Inter' : 'IBMPlexSansArabic';
return TextTheme(
displayLarge: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w700, fontSize: 57, height: 1.12),
displayMedium: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w700, fontSize: 45, height: 1.16),
displaySmall: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w700, fontSize: 36, height: 1.22),
headlineLarge: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w700, fontSize: 32, height: 1.25),
headlineMedium: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w600, fontSize: 28, height: 1.29),
headlineSmall: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w600, fontSize: 24, height: 1.33),
titleLarge: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w600, fontSize: 22, height: 1.27),
titleMedium: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w500, fontSize: 16, height: 1.50, letterSpacing: 0.15),
titleSmall: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w500, fontSize: 14, height: 1.43, letterSpacing: 0.1),
bodyLarge: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w400, fontSize: 16, height: 1.50, letterSpacing: 0.5),
bodyMedium: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w400, fontSize: 14, height: 1.43, letterSpacing: 0.25),
bodySmall: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w400, fontSize: 12, height: 1.33, letterSpacing: 0.4),
labelLarge: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w500, fontSize: 14, height: 1.43, letterSpacing: 0.1),
labelMedium: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w500, fontSize: 12, height: 1.33, letterSpacing: 0.5),
labelSmall: TextStyle(fontFamily: primary, fontFamilyFallback: [fallback], fontWeight: FontWeight.w500, fontSize: 11, height: 1.45, letterSpacing: 0.5),
);
}
+32
View File
@@ -2,6 +2,13 @@ import 'package:get_it/get_it.dart';
import 'api/api_client.dart';
import 'storage/token_store.dart';
import '../features/auth/data/auth_repository.dart';
import '../features/home/data/maps_repository.dart';
import '../features/home/data/tariff_repository.dart';
import '../features/home/data/ride_types_repository.dart';
import '../features/trip/data/trip_repository.dart';
import '../features/trip/data/trip_socket_service.dart';
import '../features/trip/data/trip_event_merger.dart';
import '../features/trip/data/fcm_service.dart';
final GetIt getIt = GetIt.instance;
@@ -12,4 +19,29 @@ Future<void> setupDi() async {
getIt.registerLazySingleton<AuthRepository>(
() => AuthRepository(getIt<ApiClient>(), getIt<TokenStore>()),
);
// ميزات Phase 1: الخرائط + الأسعار
getIt.registerLazySingleton<MapsRepository>(
() => MapsRepository(getIt<ApiClient>()),
);
getIt.registerLazySingleton<TariffRepository>(
() => TariffRepository(getIt<ApiClient>()),
);
getIt.registerLazySingleton<RideTypesRepository>(
() => RideTypesRepository(getIt<ApiClient>()),
);
// ميزات Phase 2: الرحلات + WebSocket
getIt.registerLazySingleton<TripRepository>(
() => TripRepository(getIt<ApiClient>()),
);
getIt.registerLazySingleton<TripEventMerger>(
() => TripEventMerger(),
);
getIt.registerLazySingleton<TripSocketService>(
() => TripSocketService(getIt<TokenStore>(), getIt<TripEventMerger>()),
);
getIt.registerLazySingleton<FcmService>(
() => FcmService(getIt<TripEventMerger>()),
);
}
+177
View File
@@ -0,0 +1,177 @@
{
"@@locale": "ar",
"appName": "Tripz",
"appTagline": "اسحب، وصل، وصل",
"phoneTitle": "أدخل رقم هاتفك للمتابعة",
"phoneLabel": "رقم الهاتف",
"phoneHint": "07XXXXXXXX",
"sendCode": "إرسال الرمز",
"otpTitle": "أدخل رمز التحقق",
"otpSentTo": "أُرسل إلى {phone}",
"@otpSentTo": {
"placeholders": {
"phone": {"type": "String"}
}
},
"otpLabel": "الرمز",
"confirm": "تأكيد",
"welcome": "مرحباً {name}",
"@welcome": {
"placeholders": {
"name": {"type": "String"}
}
},
"welcomeBack": "مرحباً بعودتك",
"whereTo": "إلى أين؟",
"setLocationOnMap": "تحديد الموقع على الخريطة",
"myTrips": "رحلاتي",
"wallet": "المحفظة",
"coupons": "الكوبونات والدعوات",
"settings": "الإعدادات",
"support": "الدعم",
"logout": "تسجيل الخروج",
"language": "اللغة",
"arabic": "العربية",
"english": "English",
"theme": "المظهر",
"lightMode": "الوضع النهاري",
"darkMode": "الوضع الليلي",
"systemMode": "تلقائي",
"errorGeneric": "حدث خطأ ما",
"retry": "أعد المحاولة",
"emptyGeneric": "لا توجد عناصر بعد",
"loading": "جارٍ التحميل...",
"cancel": "إلغاء",
"save": "حفظ",
"close": "إغلاق",
"next": "التالي",
"back": "رجوع",
"search": "بحث",
"noResults": "لا توجد نتائج",
"connectionError": "تحقق من اتصالك بالإنترنت",
"authError": "تعذّر إرسال الرمز",
"invalidOtp": "رمز غير صحيح",
"phoneEmpty": "أدخل رقم الهاتف",
"selectDestination": "اختر وجهتك",
"pickupLocation": "نقطة الانطلاق",
"dropoffLocation": "الوجهة",
"chooseOnMap": "اختر من الخريطة",
"favorites": "المفضلة",
"homePlace": "البيت",
"workPlace": "العمل",
"searchPlaces": "ابحث عن مكان...",
"recent": "الأخيرة",
"addStop": "إضافة توقف",
"removeStop": "إزالة التوقف",
"rideClasses": "فئة الرحلة",
"cash": "نقدي",
"walletPayment": "المحفظة",
"couponCode": "كود الخصم",
"scheduleRide": "جدولة رحلة",
"requestRide": "اطلب رحلة",
"searchingDriver": "جارٍ البحث عن سائق...",
"cancelSearch": "إلغاء البحث",
"driverAccepted": "قبل سائقك الرحلة",
"driverArriving": "السائق في طريقه إليك",
"driverArrived": "وصل سائقك",
"rideInProgress": "الرحلة جارية",
"rideCompleted": "انتهت الرحلة",
"rateDriver": "قيّم سائقك",
"tripFare": "أجرة الرحلة",
"waitingFee": "رسوم الانتظار",
"discount": "الخصم",
"paymentMethod": "طريقة الدفع",
"total": "المجموع",
"submitRating": "إرسال التقييم",
"skipRating": "تخطي",
"freeWaitMinutes": "دقائق انتظار مجانية",
"minutes": "دقائق",
"callDriver": "اتصل بالسائق",
"chatWithDriver": "دردشة مع السائق",
"shareTrip": "مشاركة الرحلة",
"emergencySOS": "طارئ SOS",
"plateNumber": "رقم اللوحة",
"vehicleInfo": "معلومات المركبة",
"driverRating": "تقييم السائق",
"eta": "الوصول المتوقع",
"km": "كم",
"min": "د",
"searchTimeout": "لم نجد سائقاً متاحاً حالياً. حاول مرة أخرى",
"priceEstimate": "تقدير السعر",
"perKm": "/كم",
"capacity": "{seats} مقاعد",
"@capacity": {
"placeholders": {
"seats": {"type": "int"}
}
},
"confirmPickup": "تأكيد نقطة الانطلاق",
"pickupSet": "تم تحديد نقطة الانطلاق",
"destinationSet": "تم تحديد الوجهة",
"routeSummary": "{distance} · {duration}",
"@routeSummary": {
"placeholders": {
"distance": {"type": "String"},
"duration": {"type": "String"}
}
},
"noPickupSet": "حدد نقطة انطلاقك",
"mapPinInstructions": "حرّك الخريطة لإظهار الدبوس",
"tripNoDrivers": "لم نجد سائقين بالقرب",
"tripCancelled": "تم إلغاء الرحلة",
"tripCancelledByDriver": "ألغى السائق الرحلة",
"cancelFee": "رسوم الإلغاء: {fee} {currency}",
"@cancelFee": {
"placeholders": {
"fee": {"type": "String"},
"currency": {"type": "String"}
}
},
"waitFreeMinutes": "{minutes} دقائق انتظار مجانية",
"@waitFreeMinutes": {
"placeholders": {
"minutes": {"type": "int"}
}
},
"waitingCharge": "رسوم الانتظار: {amount} {currency}",
"@waitingCharge": {
"placeholders": {
"amount": {"type": "String"},
"currency": {"type": "String"}
}
},
"driverEnRoute": "السائق في طريقه إليك",
"arrivingIn": "الوصول خلال {eta}",
"@arrivingIn": {
"placeholders": {
"eta": {"type": "String"}
}
},
"tripStarted": "بدأت الرحلة",
"tripEnd": "انتهت الرحلة",
"fareBreakdown": "تفاصيل الأجرة",
"baseFare": "الأجرة الأساسية",
"distanceFare": "المسافة",
"timeFare": "الوقت",
"surgeMultiplier": "الذروة",
"couponApplied": "تم تطبيق الكوبون",
"youPaid": "دفعت",
"payCash": "ادفع نقدي",
"rateYourTrip": "قيّم رحلتك",
"ratingComment": "أضف تعليقاً (اختياري)",
"thankYou": "شكراً على ملاحظاتك",
"tripDuration": "مدة الرحلة: {duration}",
"@tripDuration": {
"placeholders": {
"duration": {"type": "String"}
}
},
"tripDistance": "مسافة الرحلة: {distance}",
"@tripDistance": {
"placeholders": {
"distance": {"type": "String"}
}
},
"reconnect": "جارٍ إعادة الاتصال...",
"tripExpired": "انتهت صلاحية الرحلة"
}
+177
View File
@@ -0,0 +1,177 @@
{
"@@locale": "en",
"appName": "Tripz",
"appTagline": "Tap, ride, arrive",
"phoneTitle": "Enter your phone number to continue",
"phoneLabel": "Phone number",
"phoneHint": "07XXXXXXXX",
"sendCode": "Send code",
"otpTitle": "Enter verification code",
"otpSentTo": "Sent to {phone}",
"@otpSentTo": {
"placeholders": {
"phone": {"type": "String"}
}
},
"otpLabel": "Code",
"confirm": "Confirm",
"welcome": "Welcome {name}",
"@welcome": {
"placeholders": {
"name": {"type": "String"}
}
},
"welcomeBack": "Welcome back",
"whereTo": "Where to?",
"setLocationOnMap": "Set location on map",
"myTrips": "My trips",
"wallet": "Wallet",
"coupons": "Coupons & invites",
"settings": "Settings",
"support": "Support",
"logout": "Logout",
"language": "Language",
"arabic": "العربية",
"english": "English",
"theme": "Theme",
"lightMode": "Light",
"darkMode": "Dark",
"systemMode": "System",
"errorGeneric": "Something went wrong",
"retry": "Try again",
"emptyGeneric": "Nothing here yet",
"loading": "Loading...",
"cancel": "Cancel",
"save": "Save",
"close": "Close",
"next": "Next",
"back": "Back",
"search": "Search",
"noResults": "No results found",
"connectionError": "Check your internet connection",
"authError": "Failed to send code",
"invalidOtp": "Invalid code",
"phoneEmpty": "Enter your phone number",
"selectDestination": "Choose your destination",
"pickupLocation": "Pickup location",
"dropoffLocation": "Destination",
"chooseOnMap": "Choose on map",
"favorites": "Favorites",
"homePlace": "Home",
"workPlace": "Work",
"searchPlaces": "Search for a place...",
"recent": "Recent",
"addStop": "Add a stop",
"removeStop": "Remove stop",
"rideClasses": "Ride class",
"cash": "Cash",
"walletPayment": "Wallet",
"couponCode": "Coupon code",
"scheduleRide": "Schedule a ride",
"requestRide": "Request a ride",
"searchingDriver": "Looking for a driver...",
"cancelSearch": "Cancel search",
"driverAccepted": "Your driver accepted",
"driverArriving": "Driver is on the way",
"driverArrived": "Your driver has arrived",
"rideInProgress": "Ride in progress",
"rideCompleted": "Ride completed",
"rateDriver": "Rate your driver",
"tripFare": "Trip fare",
"waitingFee": "Waiting fee",
"discount": "Discount",
"paymentMethod": "Payment method",
"total": "Total",
"submitRating": "Submit rating",
"skipRating": "Skip",
"freeWaitMinutes": "Free wait minutes",
"minutes": "minutes",
"callDriver": "Call driver",
"chatWithDriver": "Chat with driver",
"shareTrip": "Share trip",
"emergencySOS": "Emergency SOS",
"plateNumber": "Plate number",
"vehicleInfo": "Vehicle info",
"driverRating": "Driver rating",
"eta": "Estimated arrival",
"km": "km",
"min": "min",
"searchTimeout": "No drivers available right now. Please try again",
"priceEstimate": "Price estimate",
"perKm": "/km",
"capacity": "{seats} seats",
"@capacity": {
"placeholders": {
"seats": {"type": "int"}
}
},
"confirmPickup": "Confirm pickup",
"pickupSet": "Pickup set",
"destinationSet": "Destination set",
"routeSummary": "{distance} · {duration}",
"@routeSummary": {
"placeholders": {
"distance": {"type": "String"},
"duration": {"type": "String"}
}
},
"noPickupSet": "Set your pickup location",
"mapPinInstructions": "Move the map to place the pin",
"tripNoDrivers": "No drivers found nearby",
"tripCancelled": "Trip cancelled",
"tripCancelledByDriver": "Driver cancelled the trip",
"cancelFee": "Cancellation fee: {fee} {currency}",
"@cancelFee": {
"placeholders": {
"fee": {"type": "String"},
"currency": {"type": "String"}
}
},
"waitFreeMinutes": "{minutes} min free wait",
"@waitFreeMinutes": {
"placeholders": {
"minutes": {"type": "int"}
}
},
"waitingCharge": "Waiting charge: {amount} {currency}",
"@waitingCharge": {
"placeholders": {
"amount": {"type": "String"},
"currency": {"type": "String"}
}
},
"driverEnRoute": "Driver is heading to you",
"arrivingIn": "Arriving in {eta}",
"@arrivingIn": {
"placeholders": {
"eta": {"type": "String"}
}
},
"tripStarted": "Ride started",
"tripEnd": "Trip ended",
"fareBreakdown": "Fare breakdown",
"baseFare": "Base fare",
"distanceFare": "Distance",
"timeFare": "Time",
"surgeMultiplier": "Surge",
"couponApplied": "Coupon applied",
"youPaid": "You paid",
"payCash": "Pay cash",
"rateYourTrip": "Rate your trip",
"ratingComment": "Add a comment (optional)",
"thankYou": "Thank you for your feedback",
"tripDuration": "Trip duration: {duration}",
"@tripDuration": {
"placeholders": {
"duration": {"type": "String"}
}
},
"tripDistance": "Trip distance: {distance}",
"@tripDistance": {
"placeholders": {
"distance": {"type": "String"}
}
},
"reconnect": "Reconnecting...",
"tripExpired": "Trip expired"
}
@@ -0,0 +1,859 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_ar.dart';
import 'app_localizations_en.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, you’ll need to edit this
/// file.
///
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// project’s Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('ar'),
Locale('en')
];
/// No description provided for @appName.
///
/// In ar, this message translates to:
/// **'Tripz'**
String get appName;
/// No description provided for @appTagline.
///
/// In ar, this message translates to:
/// **'اسحب، وصل، وصل'**
String get appTagline;
/// No description provided for @phoneTitle.
///
/// In ar, this message translates to:
/// **'أدخل رقم هاتفك للمتابعة'**
String get phoneTitle;
/// No description provided for @phoneLabel.
///
/// In ar, this message translates to:
/// **'رقم الهاتف'**
String get phoneLabel;
/// No description provided for @phoneHint.
///
/// In ar, this message translates to:
/// **'07XXXXXXXX'**
String get phoneHint;
/// No description provided for @sendCode.
///
/// In ar, this message translates to:
/// **'إرسال الرمز'**
String get sendCode;
/// No description provided for @otpTitle.
///
/// In ar, this message translates to:
/// **'أدخل رمز التحقق'**
String get otpTitle;
/// No description provided for @otpSentTo.
///
/// In ar, this message translates to:
/// **'أُرسل إلى {phone}'**
String otpSentTo(String phone);
/// No description provided for @otpLabel.
///
/// In ar, this message translates to:
/// **'الرمز'**
String get otpLabel;
/// No description provided for @confirm.
///
/// In ar, this message translates to:
/// **'تأكيد'**
String get confirm;
/// No description provided for @welcome.
///
/// In ar, this message translates to:
/// **'مرحباً {name}'**
String welcome(String name);
/// No description provided for @welcomeBack.
///
/// In ar, this message translates to:
/// **'مرحباً بعودتك'**
String get welcomeBack;
/// No description provided for @whereTo.
///
/// In ar, this message translates to:
/// **'إلى أين؟'**
String get whereTo;
/// No description provided for @setLocationOnMap.
///
/// In ar, this message translates to:
/// **'تحديد الموقع على الخريطة'**
String get setLocationOnMap;
/// No description provided for @myTrips.
///
/// In ar, this message translates to:
/// **'رحلاتي'**
String get myTrips;
/// No description provided for @wallet.
///
/// In ar, this message translates to:
/// **'المحفظة'**
String get wallet;
/// No description provided for @coupons.
///
/// In ar, this message translates to:
/// **'الكوبونات والدعوات'**
String get coupons;
/// No description provided for @settings.
///
/// In ar, this message translates to:
/// **'الإعدادات'**
String get settings;
/// No description provided for @support.
///
/// In ar, this message translates to:
/// **'الدعم'**
String get support;
/// No description provided for @logout.
///
/// In ar, this message translates to:
/// **'تسجيل الخروج'**
String get logout;
/// No description provided for @language.
///
/// In ar, this message translates to:
/// **'اللغة'**
String get language;
/// No description provided for @arabic.
///
/// In ar, this message translates to:
/// **'العربية'**
String get arabic;
/// No description provided for @english.
///
/// In ar, this message translates to:
/// **'English'**
String get english;
/// No description provided for @theme.
///
/// In ar, this message translates to:
/// **'المظهر'**
String get theme;
/// No description provided for @lightMode.
///
/// In ar, this message translates to:
/// **'الوضع النهاري'**
String get lightMode;
/// No description provided for @darkMode.
///
/// In ar, this message translates to:
/// **'الوضع الليلي'**
String get darkMode;
/// No description provided for @systemMode.
///
/// In ar, this message translates to:
/// **'تلقائي'**
String get systemMode;
/// No description provided for @errorGeneric.
///
/// In ar, this message translates to:
/// **'حدث خطأ ما'**
String get errorGeneric;
/// No description provided for @retry.
///
/// In ar, this message translates to:
/// **'أعد المحاولة'**
String get retry;
/// No description provided for @emptyGeneric.
///
/// In ar, this message translates to:
/// **'لا توجد عناصر بعد'**
String get emptyGeneric;
/// No description provided for @loading.
///
/// In ar, this message translates to:
/// **'جارٍ التحميل...'**
String get loading;
/// No description provided for @cancel.
///
/// In ar, this message translates to:
/// **'إلغاء'**
String get cancel;
/// No description provided for @save.
///
/// In ar, this message translates to:
/// **'حفظ'**
String get save;
/// No description provided for @close.
///
/// In ar, this message translates to:
/// **'إغلاق'**
String get close;
/// No description provided for @next.
///
/// In ar, this message translates to:
/// **'التالي'**
String get next;
/// No description provided for @back.
///
/// In ar, this message translates to:
/// **'رجوع'**
String get back;
/// No description provided for @search.
///
/// In ar, this message translates to:
/// **'بحث'**
String get search;
/// No description provided for @noResults.
///
/// In ar, this message translates to:
/// **'لا توجد نتائج'**
String get noResults;
/// No description provided for @connectionError.
///
/// In ar, this message translates to:
/// **'تحقق من اتصالك بالإنترنت'**
String get connectionError;
/// No description provided for @authError.
///
/// In ar, this message translates to:
/// **'تعذّر إرسال الرمز'**
String get authError;
/// No description provided for @invalidOtp.
///
/// In ar, this message translates to:
/// **'رمز غير صحيح'**
String get invalidOtp;
/// No description provided for @phoneEmpty.
///
/// In ar, this message translates to:
/// **'أدخل رقم الهاتف'**
String get phoneEmpty;
/// No description provided for @selectDestination.
///
/// In ar, this message translates to:
/// **'اختر وجهتك'**
String get selectDestination;
/// No description provided for @pickupLocation.
///
/// In ar, this message translates to:
/// **'نقطة الانطلاق'**
String get pickupLocation;
/// No description provided for @dropoffLocation.
///
/// In ar, this message translates to:
/// **'الوجهة'**
String get dropoffLocation;
/// No description provided for @chooseOnMap.
///
/// In ar, this message translates to:
/// **'اختر من الخريطة'**
String get chooseOnMap;
/// No description provided for @favorites.
///
/// In ar, this message translates to:
/// **'المفضلة'**
String get favorites;
/// No description provided for @homePlace.
///
/// In ar, this message translates to:
/// **'البيت'**
String get homePlace;
/// No description provided for @workPlace.
///
/// In ar, this message translates to:
/// **'العمل'**
String get workPlace;
/// No description provided for @searchPlaces.
///
/// In ar, this message translates to:
/// **'ابحث عن مكان...'**
String get searchPlaces;
/// No description provided for @recent.
///
/// In ar, this message translates to:
/// **'الأخيرة'**
String get recent;
/// No description provided for @addStop.
///
/// In ar, this message translates to:
/// **'إضافة توقف'**
String get addStop;
/// No description provided for @removeStop.
///
/// In ar, this message translates to:
/// **'إزالة التوقف'**
String get removeStop;
/// No description provided for @rideClasses.
///
/// In ar, this message translates to:
/// **'فئة الرحلة'**
String get rideClasses;
/// No description provided for @cash.
///
/// In ar, this message translates to:
/// **'نقدي'**
String get cash;
/// No description provided for @walletPayment.
///
/// In ar, this message translates to:
/// **'المحفظة'**
String get walletPayment;
/// No description provided for @couponCode.
///
/// In ar, this message translates to:
/// **'كود الخصم'**
String get couponCode;
/// No description provided for @scheduleRide.
///
/// In ar, this message translates to:
/// **'جدولة رحلة'**
String get scheduleRide;
/// No description provided for @requestRide.
///
/// In ar, this message translates to:
/// **'اطلب رحلة'**
String get requestRide;
/// No description provided for @searchingDriver.
///
/// In ar, this message translates to:
/// **'جارٍ البحث عن سائق...'**
String get searchingDriver;
/// No description provided for @cancelSearch.
///
/// In ar, this message translates to:
/// **'إلغاء البحث'**
String get cancelSearch;
/// No description provided for @driverAccepted.
///
/// In ar, this message translates to:
/// **'قبل سائقك الرحلة'**
String get driverAccepted;
/// No description provided for @driverArriving.
///
/// In ar, this message translates to:
/// **'السائق في طريقه إليك'**
String get driverArriving;
/// No description provided for @driverArrived.
///
/// In ar, this message translates to:
/// **'وصل سائقك'**
String get driverArrived;
/// No description provided for @rideInProgress.
///
/// In ar, this message translates to:
/// **'الرحلة جارية'**
String get rideInProgress;
/// No description provided for @rideCompleted.
///
/// In ar, this message translates to:
/// **'انتهت الرحلة'**
String get rideCompleted;
/// No description provided for @rateDriver.
///
/// In ar, this message translates to:
/// **'قيّم سائقك'**
String get rateDriver;
/// No description provided for @tripFare.
///
/// In ar, this message translates to:
/// **'أجرة الرحلة'**
String get tripFare;
/// No description provided for @waitingFee.
///
/// In ar, this message translates to:
/// **'رسوم الانتظار'**
String get waitingFee;
/// No description provided for @discount.
///
/// In ar, this message translates to:
/// **'الخصم'**
String get discount;
/// No description provided for @paymentMethod.
///
/// In ar, this message translates to:
/// **'طريقة الدفع'**
String get paymentMethod;
/// No description provided for @total.
///
/// In ar, this message translates to:
/// **'المجموع'**
String get total;
/// No description provided for @submitRating.
///
/// In ar, this message translates to:
/// **'إرسال التقييم'**
String get submitRating;
/// No description provided for @skipRating.
///
/// In ar, this message translates to:
/// **'تخطي'**
String get skipRating;
/// No description provided for @freeWaitMinutes.
///
/// In ar, this message translates to:
/// **'دقائق انتظار مجانية'**
String get freeWaitMinutes;
/// No description provided for @minutes.
///
/// In ar, this message translates to:
/// **'دقائق'**
String get minutes;
/// No description provided for @callDriver.
///
/// In ar, this message translates to:
/// **'اتصل بالسائق'**
String get callDriver;
/// No description provided for @chatWithDriver.
///
/// In ar, this message translates to:
/// **'دردشة مع السائق'**
String get chatWithDriver;
/// No description provided for @shareTrip.
///
/// In ar, this message translates to:
/// **'مشاركة الرحلة'**
String get shareTrip;
/// No description provided for @emergencySOS.
///
/// In ar, this message translates to:
/// **'طارئ SOS'**
String get emergencySOS;
/// No description provided for @plateNumber.
///
/// In ar, this message translates to:
/// **'رقم اللوحة'**
String get plateNumber;
/// No description provided for @vehicleInfo.
///
/// In ar, this message translates to:
/// **'معلومات المركبة'**
String get vehicleInfo;
/// No description provided for @driverRating.
///
/// In ar, this message translates to:
/// **'تقييم السائق'**
String get driverRating;
/// No description provided for @eta.
///
/// In ar, this message translates to:
/// **'الوصول المتوقع'**
String get eta;
/// No description provided for @km.
///
/// In ar, this message translates to:
/// **'كم'**
String get km;
/// No description provided for @min.
///
/// In ar, this message translates to:
/// **'د'**
String get min;
/// No description provided for @searchTimeout.
///
/// In ar, this message translates to:
/// **'لم نجد سائقاً متاحاً حالياً. حاول مرة أخرى'**
String get searchTimeout;
/// No description provided for @priceEstimate.
///
/// In ar, this message translates to:
/// **'تقدير السعر'**
String get priceEstimate;
/// No description provided for @perKm.
///
/// In ar, this message translates to:
/// **'/كم'**
String get perKm;
/// No description provided for @capacity.
///
/// In ar, this message translates to:
/// **'{seats} مقاعد'**
String capacity(int seats);
/// No description provided for @confirmPickup.
///
/// In ar, this message translates to:
/// **'تأكيد نقطة الانطلاق'**
String get confirmPickup;
/// No description provided for @pickupSet.
///
/// In ar, this message translates to:
/// **'تم تحديد نقطة الانطلاق'**
String get pickupSet;
/// No description provided for @destinationSet.
///
/// In ar, this message translates to:
/// **'تم تحديد الوجهة'**
String get destinationSet;
/// No description provided for @routeSummary.
///
/// In ar, this message translates to:
/// **'{distance} · {duration}'**
String routeSummary(String distance, String duration);
/// No description provided for @noPickupSet.
///
/// In ar, this message translates to:
/// **'حدد نقطة انطلاقك'**
String get noPickupSet;
/// No description provided for @mapPinInstructions.
///
/// In ar, this message translates to:
/// **'حرّك الخريطة لإظهار الدبوس'**
String get mapPinInstructions;
/// No description provided for @tripNoDrivers.
///
/// In ar, this message translates to:
/// **'لم نجد سائقين بالقرب'**
String get tripNoDrivers;
/// No description provided for @tripCancelled.
///
/// In ar, this message translates to:
/// **'تم إلغاء الرحلة'**
String get tripCancelled;
/// No description provided for @tripCancelledByDriver.
///
/// In ar, this message translates to:
/// **'ألغى السائق الرحلة'**
String get tripCancelledByDriver;
/// No description provided for @cancelFee.
///
/// In ar, this message translates to:
/// **'رسوم الإلغاء: {fee} {currency}'**
String cancelFee(String fee, String currency);
/// No description provided for @waitFreeMinutes.
///
/// In ar, this message translates to:
/// **'{minutes} دقائق انتظار مجانية'**
String waitFreeMinutes(int minutes);
/// No description provided for @waitingCharge.
///
/// In ar, this message translates to:
/// **'رسوم الانتظار: {amount} {currency}'**
String waitingCharge(String amount, String currency);
/// No description provided for @driverEnRoute.
///
/// In ar, this message translates to:
/// **'السائق في طريقه إليك'**
String get driverEnRoute;
/// No description provided for @arrivingIn.
///
/// In ar, this message translates to:
/// **'الوصول خلال {eta}'**
String arrivingIn(String eta);
/// No description provided for @tripStarted.
///
/// In ar, this message translates to:
/// **'بدأت الرحلة'**
String get tripStarted;
/// No description provided for @tripEnd.
///
/// In ar, this message translates to:
/// **'انتهت الرحلة'**
String get tripEnd;
/// No description provided for @fareBreakdown.
///
/// In ar, this message translates to:
/// **'تفاصيل الأجرة'**
String get fareBreakdown;
/// No description provided for @baseFare.
///
/// In ar, this message translates to:
/// **'الأجرة الأساسية'**
String get baseFare;
/// No description provided for @distanceFare.
///
/// In ar, this message translates to:
/// **'المسافة'**
String get distanceFare;
/// No description provided for @timeFare.
///
/// In ar, this message translates to:
/// **'الوقت'**
String get timeFare;
/// No description provided for @surgeMultiplier.
///
/// In ar, this message translates to:
/// **'الذروة'**
String get surgeMultiplier;
/// No description provided for @couponApplied.
///
/// In ar, this message translates to:
/// **'تم تطبيق الكوبون'**
String get couponApplied;
/// No description provided for @youPaid.
///
/// In ar, this message translates to:
/// **'دفعت'**
String get youPaid;
/// No description provided for @payCash.
///
/// In ar, this message translates to:
/// **'ادفع نقدي'**
String get payCash;
/// No description provided for @rateYourTrip.
///
/// In ar, this message translates to:
/// **'قيّم رحلتك'**
String get rateYourTrip;
/// No description provided for @ratingComment.
///
/// In ar, this message translates to:
/// **'أضف تعليقاً (اختياري)'**
String get ratingComment;
/// No description provided for @thankYou.
///
/// In ar, this message translates to:
/// **'شكراً على ملاحظاتك'**
String get thankYou;
/// No description provided for @tripDuration.
///
/// In ar, this message translates to:
/// **'مدة الرحلة: {duration}'**
String tripDuration(String duration);
/// No description provided for @tripDistance.
///
/// In ar, this message translates to:
/// **'مسافة الرحلة: {distance}'**
String tripDistance(String distance);
/// No description provided for @reconnect.
///
/// In ar, this message translates to:
/// **'جارٍ إعادة الاتصال...'**
String get reconnect;
/// No description provided for @tripExpired.
///
/// In ar, this message translates to:
/// **'انتهت صلاحية الرحلة'**
String get tripExpired;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['ar', 'en'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'ar':
return AppLocalizationsAr();
case 'en':
return AppLocalizationsEn();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.');
}
@@ -0,0 +1,393 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Arabic (`ar`).
class AppLocalizationsAr extends AppLocalizations {
AppLocalizationsAr([String locale = 'ar']) : super(locale);
@override
String get appName => 'Tripz';
@override
String get appTagline => 'اسحب، وصل، وصل';
@override
String get phoneTitle => 'أدخل رقم هاتفك للمتابعة';
@override
String get phoneLabel => 'رقم الهاتف';
@override
String get phoneHint => '07XXXXXXXX';
@override
String get sendCode => 'إرسال الرمز';
@override
String get otpTitle => 'أدخل رمز التحقق';
@override
String otpSentTo(String phone) {
return 'أُرسل إلى $phone';
}
@override
String get otpLabel => 'الرمز';
@override
String get confirm => 'تأكيد';
@override
String welcome(String name) {
return 'مرحباً $name';
}
@override
String get welcomeBack => 'مرحباً بعودتك';
@override
String get whereTo => 'إلى أين؟';
@override
String get setLocationOnMap => 'تحديد الموقع على الخريطة';
@override
String get myTrips => 'رحلاتي';
@override
String get wallet => 'المحفظة';
@override
String get coupons => 'الكوبونات والدعوات';
@override
String get settings => 'الإعدادات';
@override
String get support => 'الدعم';
@override
String get logout => 'تسجيل الخروج';
@override
String get language => 'اللغة';
@override
String get arabic => 'العربية';
@override
String get english => 'English';
@override
String get theme => 'المظهر';
@override
String get lightMode => 'الوضع النهاري';
@override
String get darkMode => 'الوضع الليلي';
@override
String get systemMode => 'تلقائي';
@override
String get errorGeneric => 'حدث خطأ ما';
@override
String get retry => 'أعد المحاولة';
@override
String get emptyGeneric => 'لا توجد عناصر بعد';
@override
String get loading => 'جارٍ التحميل...';
@override
String get cancel => 'إلغاء';
@override
String get save => 'حفظ';
@override
String get close => 'إغلاق';
@override
String get next => 'التالي';
@override
String get back => 'رجوع';
@override
String get search => 'بحث';
@override
String get noResults => 'لا توجد نتائج';
@override
String get connectionError => 'تحقق من اتصالك بالإنترنت';
@override
String get authError => 'تعذّر إرسال الرمز';
@override
String get invalidOtp => 'رمز غير صحيح';
@override
String get phoneEmpty => 'أدخل رقم الهاتف';
@override
String get selectDestination => 'اختر وجهتك';
@override
String get pickupLocation => 'نقطة الانطلاق';
@override
String get dropoffLocation => 'الوجهة';
@override
String get chooseOnMap => 'اختر من الخريطة';
@override
String get favorites => 'المفضلة';
@override
String get homePlace => 'البيت';
@override
String get workPlace => 'العمل';
@override
String get searchPlaces => 'ابحث عن مكان...';
@override
String get recent => 'الأخيرة';
@override
String get addStop => 'إضافة توقف';
@override
String get removeStop => 'إزالة التوقف';
@override
String get rideClasses => 'فئة الرحلة';
@override
String get cash => 'نقدي';
@override
String get walletPayment => 'المحفظة';
@override
String get couponCode => 'كود الخصم';
@override
String get scheduleRide => 'جدولة رحلة';
@override
String get requestRide => 'اطلب رحلة';
@override
String get searchingDriver => 'جارٍ البحث عن سائق...';
@override
String get cancelSearch => 'إلغاء البحث';
@override
String get driverAccepted => 'قبل سائقك الرحلة';
@override
String get driverArriving => 'السائق في طريقه إليك';
@override
String get driverArrived => 'وصل سائقك';
@override
String get rideInProgress => 'الرحلة جارية';
@override
String get rideCompleted => 'انتهت الرحلة';
@override
String get rateDriver => 'قيّم سائقك';
@override
String get tripFare => 'أجرة الرحلة';
@override
String get waitingFee => 'رسوم الانتظار';
@override
String get discount => 'الخصم';
@override
String get paymentMethod => 'طريقة الدفع';
@override
String get total => 'المجموع';
@override
String get submitRating => 'إرسال التقييم';
@override
String get skipRating => 'تخطي';
@override
String get freeWaitMinutes => 'دقائق انتظار مجانية';
@override
String get minutes => 'دقائق';
@override
String get callDriver => 'اتصل بالسائق';
@override
String get chatWithDriver => 'دردشة مع السائق';
@override
String get shareTrip => 'مشاركة الرحلة';
@override
String get emergencySOS => 'طارئ SOS';
@override
String get plateNumber => 'رقم اللوحة';
@override
String get vehicleInfo => 'معلومات المركبة';
@override
String get driverRating => 'تقييم السائق';
@override
String get eta => 'الوصول المتوقع';
@override
String get km => 'كم';
@override
String get min => 'د';
@override
String get searchTimeout => 'لم نجد سائقاً متاحاً حالياً. حاول مرة أخرى';
@override
String get priceEstimate => 'تقدير السعر';
@override
String get perKm => '/كم';
@override
String capacity(int seats) {
return '$seats مقاعد';
}
@override
String get confirmPickup => 'تأكيد نقطة الانطلاق';
@override
String get pickupSet => 'تم تحديد نقطة الانطلاق';
@override
String get destinationSet => 'تم تحديد الوجهة';
@override
String routeSummary(String distance, String duration) {
return '$distance · $duration';
}
@override
String get noPickupSet => 'حدد نقطة انطلاقك';
@override
String get mapPinInstructions => 'حرّك الخريطة لإظهار الدبوس';
@override
String get tripNoDrivers => 'لم نجد سائقين بالقرب';
@override
String get tripCancelled => 'تم إلغاء الرحلة';
@override
String get tripCancelledByDriver => 'ألغى السائق الرحلة';
@override
String cancelFee(String fee, String currency) {
return 'رسوم الإلغاء: $fee $currency';
}
@override
String waitFreeMinutes(int minutes) {
return '$minutes دقائق انتظار مجانية';
}
@override
String waitingCharge(String amount, String currency) {
return 'رسوم الانتظار: $amount $currency';
}
@override
String get driverEnRoute => 'السائق في طريقه إليك';
@override
String arrivingIn(String eta) {
return 'الوصول خلال $eta';
}
@override
String get tripStarted => 'بدأت الرحلة';
@override
String get tripEnd => 'انتهت الرحلة';
@override
String get fareBreakdown => 'تفاصيل الأجرة';
@override
String get baseFare => 'الأجرة الأساسية';
@override
String get distanceFare => 'المسافة';
@override
String get timeFare => 'الوقت';
@override
String get surgeMultiplier => 'الذروة';
@override
String get couponApplied => 'تم تطبيق الكوبون';
@override
String get youPaid => 'دفعت';
@override
String get payCash => 'ادفع نقدي';
@override
String get rateYourTrip => 'قيّم رحلتك';
@override
String get ratingComment => 'أضف تعليقاً (اختياري)';
@override
String get thankYou => 'شكراً على ملاحظاتك';
@override
String tripDuration(String duration) {
return 'مدة الرحلة: $duration';
}
@override
String tripDistance(String distance) {
return 'مسافة الرحلة: $distance';
}
@override
String get reconnect => 'جارٍ إعادة الاتصال...';
@override
String get tripExpired => 'انتهت صلاحية الرحلة';
}
@@ -0,0 +1,394 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appName => 'Tripz';
@override
String get appTagline => 'Tap, ride, arrive';
@override
String get phoneTitle => 'Enter your phone number to continue';
@override
String get phoneLabel => 'Phone number';
@override
String get phoneHint => '07XXXXXXXX';
@override
String get sendCode => 'Send code';
@override
String get otpTitle => 'Enter verification code';
@override
String otpSentTo(String phone) {
return 'Sent to $phone';
}
@override
String get otpLabel => 'Code';
@override
String get confirm => 'Confirm';
@override
String welcome(String name) {
return 'Welcome $name';
}
@override
String get welcomeBack => 'Welcome back';
@override
String get whereTo => 'Where to?';
@override
String get setLocationOnMap => 'Set location on map';
@override
String get myTrips => 'My trips';
@override
String get wallet => 'Wallet';
@override
String get coupons => 'Coupons & invites';
@override
String get settings => 'Settings';
@override
String get support => 'Support';
@override
String get logout => 'Logout';
@override
String get language => 'Language';
@override
String get arabic => 'العربية';
@override
String get english => 'English';
@override
String get theme => 'Theme';
@override
String get lightMode => 'Light';
@override
String get darkMode => 'Dark';
@override
String get systemMode => 'System';
@override
String get errorGeneric => 'Something went wrong';
@override
String get retry => 'Try again';
@override
String get emptyGeneric => 'Nothing here yet';
@override
String get loading => 'Loading...';
@override
String get cancel => 'Cancel';
@override
String get save => 'Save';
@override
String get close => 'Close';
@override
String get next => 'Next';
@override
String get back => 'Back';
@override
String get search => 'Search';
@override
String get noResults => 'No results found';
@override
String get connectionError => 'Check your internet connection';
@override
String get authError => 'Failed to send code';
@override
String get invalidOtp => 'Invalid code';
@override
String get phoneEmpty => 'Enter your phone number';
@override
String get selectDestination => 'Choose your destination';
@override
String get pickupLocation => 'Pickup location';
@override
String get dropoffLocation => 'Destination';
@override
String get chooseOnMap => 'Choose on map';
@override
String get favorites => 'Favorites';
@override
String get homePlace => 'Home';
@override
String get workPlace => 'Work';
@override
String get searchPlaces => 'Search for a place...';
@override
String get recent => 'Recent';
@override
String get addStop => 'Add a stop';
@override
String get removeStop => 'Remove stop';
@override
String get rideClasses => 'Ride class';
@override
String get cash => 'Cash';
@override
String get walletPayment => 'Wallet';
@override
String get couponCode => 'Coupon code';
@override
String get scheduleRide => 'Schedule a ride';
@override
String get requestRide => 'Request a ride';
@override
String get searchingDriver => 'Looking for a driver...';
@override
String get cancelSearch => 'Cancel search';
@override
String get driverAccepted => 'Your driver accepted';
@override
String get driverArriving => 'Driver is on the way';
@override
String get driverArrived => 'Your driver has arrived';
@override
String get rideInProgress => 'Ride in progress';
@override
String get rideCompleted => 'Ride completed';
@override
String get rateDriver => 'Rate your driver';
@override
String get tripFare => 'Trip fare';
@override
String get waitingFee => 'Waiting fee';
@override
String get discount => 'Discount';
@override
String get paymentMethod => 'Payment method';
@override
String get total => 'Total';
@override
String get submitRating => 'Submit rating';
@override
String get skipRating => 'Skip';
@override
String get freeWaitMinutes => 'Free wait minutes';
@override
String get minutes => 'minutes';
@override
String get callDriver => 'Call driver';
@override
String get chatWithDriver => 'Chat with driver';
@override
String get shareTrip => 'Share trip';
@override
String get emergencySOS => 'Emergency SOS';
@override
String get plateNumber => 'Plate number';
@override
String get vehicleInfo => 'Vehicle info';
@override
String get driverRating => 'Driver rating';
@override
String get eta => 'Estimated arrival';
@override
String get km => 'km';
@override
String get min => 'min';
@override
String get searchTimeout =>
'No drivers available right now. Please try again';
@override
String get priceEstimate => 'Price estimate';
@override
String get perKm => '/km';
@override
String capacity(int seats) {
return '$seats seats';
}
@override
String get confirmPickup => 'Confirm pickup';
@override
String get pickupSet => 'Pickup set';
@override
String get destinationSet => 'Destination set';
@override
String routeSummary(String distance, String duration) {
return '$distance · $duration';
}
@override
String get noPickupSet => 'Set your pickup location';
@override
String get mapPinInstructions => 'Move the map to place the pin';
@override
String get tripNoDrivers => 'No drivers found nearby';
@override
String get tripCancelled => 'Trip cancelled';
@override
String get tripCancelledByDriver => 'Driver cancelled the trip';
@override
String cancelFee(String fee, String currency) {
return 'Cancellation fee: $fee $currency';
}
@override
String waitFreeMinutes(int minutes) {
return '$minutes min free wait';
}
@override
String waitingCharge(String amount, String currency) {
return 'Waiting charge: $amount $currency';
}
@override
String get driverEnRoute => 'Driver is heading to you';
@override
String arrivingIn(String eta) {
return 'Arriving in $eta';
}
@override
String get tripStarted => 'Ride started';
@override
String get tripEnd => 'Trip ended';
@override
String get fareBreakdown => 'Fare breakdown';
@override
String get baseFare => 'Base fare';
@override
String get distanceFare => 'Distance';
@override
String get timeFare => 'Time';
@override
String get surgeMultiplier => 'Surge';
@override
String get couponApplied => 'Coupon applied';
@override
String get youPaid => 'You paid';
@override
String get payCash => 'Pay cash';
@override
String get rateYourTrip => 'Rate your trip';
@override
String get ratingComment => 'Add a comment (optional)';
@override
String get thankYou => 'Thank you for your feedback';
@override
String tripDuration(String duration) {
return 'Trip duration: $duration';
}
@override
String tripDistance(String distance) {
return 'Trip distance: $distance';
}
@override
String get reconnect => 'Reconnecting...';
@override
String get tripExpired => 'Trip expired';
}
+48 -2
View File
@@ -1,11 +1,24 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../features/auth/cubit/auth_cubit.dart';
import '../features/auth/cubit/auth_state.dart';
import '../features/auth/view/phone_screen.dart';
import '../features/auth/view/otp_screen.dart';
import '../features/home/view/home_screen.dart';
import '../features/home/view/destination_screen.dart';
import '../features/home/view/route_preview_screen.dart';
import '../features/trip/view/trip_tracking_screen.dart';
import '../features/trip/view/trip_invoice_screen.dart';
import '../features/trip/view/rating_screen.dart';
import '../features/home/cubit/home_cubit.dart';
import '../features/home/cubit/destination_cubit.dart';
import '../features/home/cubit/quote_cubit.dart';
import '../features/home/data/models/place_model.dart';
import '../features/home/data/maps_repository.dart';
import '../features/home/data/tariff_repository.dart';
import 'di.dart';
GoRouter buildRouter(AuthCubit auth) {
return GoRouter(
@@ -26,7 +39,40 @@ GoRouter buildRouter(AuthCubit auth) {
routes: [
GoRoute(path: '/phone', builder: (_, __) => const PhoneScreen()),
GoRoute(path: '/otp', builder: (_, __) => const OtpScreen()),
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
GoRoute(
path: '/home',
builder: (_, __) => MultiBlocProvider(
providers: [
BlocProvider(create: (_) => HomeCubit(getIt<MapsRepository>())),
],
child: const HomeScreen(),
),
),
GoRoute(
path: '/destination',
builder: (_, __) => BlocProvider(
create: (_) => DestinationCubit(getIt<MapsRepository>()),
child: const DestinationScreen(),
),
),
GoRoute(
path: '/route-preview',
builder: (_, state) {
final extra = state.extra as Map<String, dynamic>?;
final pickup = extra?['pickup'] as PlaceModel?;
final destination = extra?['destination'] as PlaceModel?;
if (pickup == null || destination == null) {
return const Scaffold(body: Center(child: Text('Missing data')));
}
return BlocProvider(
create: (_) => QuoteCubit(getIt<TariffRepository>()),
child: RoutePreviewScreen(pickup: pickup, destination: destination),
);
},
),
GoRoute(path: '/trip-tracking', builder: (_, __) => const TripTrackingScreen()),
GoRoute(path: '/trip-invoice', builder: (_, __) => const TripInvoiceScreen()),
GoRoute(path: '/trip-rating', builder: (_, __) => const RatingScreen()),
],
);
}
-21
View File
@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
/// ثيم Tripz — أساس بسيط، خط عربي (mohanad) وألوان.
ThemeData buildTheme() {
const seed = Color(0xFF0E7C66); // أخضر تركوازي
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: seed),
fontFamily: 'mohanad',
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
);
}
+39
View File
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// حالة اللاشيء — رسالة + أيقونة.
/// docs/26 §7
class EmptyView extends StatelessWidget {
const EmptyView({
super.key,
this.icon = Icons.inbox_rounded,
this.message,
});
final IconData icon;
final String? message;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(T.s32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4)),
const SizedBox(height: T.s16),
Text(
message ?? 'لا توجد عناصر بعد',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: cs.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
+48
View File
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// حالة الخطأ — رسالة بالعربية + زر إعادة المحاولة.
/// docs/26 §7
class ErrorView extends StatelessWidget {
const ErrorView({
super.key,
this.message,
this.onRetry,
});
final String? message;
final VoidCallback? onRetry;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(T.s32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline_rounded,
size: 64, color: cs.error.withValues(alpha: 0.6)),
const SizedBox(height: T.s16),
Text(
message ?? 'حدث خطأ ما',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: cs.onSurface,
),
textAlign: TextAlign.center,
),
if (onRetry != null) ...[
const SizedBox(height: T.s16),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh_rounded, size: 20),
label: const Text('أعد المحاولة'),
),
],
],
),
),
);
}
}
+104
View File
@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// هيكل التحميل — تأثير نبض على عناصر مستوحاة من الشاشة الفعلية.
/// docs/26 §7
class Skeleton extends StatelessWidget {
const Skeleton({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final baseColor = cs.surfaceContainerHighest;
final highlightColor = cs.surfaceContainerHigh;
return Padding(
padding: const EdgeInsets.all(T.pagePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_PulseBox(width: double.infinity, height: 200, base: baseColor, highlight: highlightColor),
const SizedBox(height: T.s16),
_PulseBox(width: 200, height: 20, base: baseColor, highlight: highlightColor),
const SizedBox(height: T.s12),
_PulseBox(width: 140, height: 16, base: baseColor, highlight: highlightColor),
const SizedBox(height: T.s24),
Row(
children: [
_PulseBox(width: 48, height: 48, base: baseColor, highlight: highlightColor, radius: T.rCircle),
const SizedBox(width: T.s12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_PulseBox(width: double.infinity, height: 16, base: baseColor, highlight: highlightColor),
const SizedBox(height: T.s8),
_PulseBox(width: 120, height: 14, base: baseColor, highlight: highlightColor),
],
),
),
],
),
],
),
);
}
}
class _PulseBox extends StatefulWidget {
const _PulseBox({
required this.width,
required this.height,
required this.base,
required this.highlight,
this.radius = 8,
});
final double width;
final double height;
final Color base;
final Color highlight;
final double radius;
@override
State<_PulseBox> createState() => _PulseBoxState();
}
class _PulseBoxState extends State<_PulseBox>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<double> _anim;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))
..repeat();
_anim = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _anim,
builder: (_, __) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: Color.lerp(widget.base, widget.highlight, _anim.value),
borderRadius: BorderRadius.circular(widget.radius),
),
);
},
);
}
}
+51
View File
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// شريط حالة الرحلة الحي.
/// docs/26 §7
class StatusBanner extends StatelessWidget {
const StatusBanner({
super.key,
required this.label,
this.color,
this.icon,
});
final String label;
final Color? color;
final IconData? icon;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final effectiveColor = color ?? cs.primary;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: T.s16,
vertical: T.s12,
),
decoration: BoxDecoration(
color: effectiveColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(T.r8),
),
child: Row(
children: [
if (icon != null) ...[
Icon(icon, color: effectiveColor, size: T.iconSm),
const SizedBox(width: T.s8),
],
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: effectiveColor,
),
),
),
],
),
);
}
}
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// زر موحّد: primary (معبّأ) · secondary (محدّد) · text — مع حالة تحميل داخلية.
/// docs/26 §7
enum TripzButtonStyle { primary, secondary, text }
class TripzButton extends StatelessWidget {
const TripzButton({
super.key,
required this.label,
this.onPressed,
this.style = TripzButtonStyle.primary,
this.loading = false,
this.icon,
this.fullWidth = true,
});
final String label;
final VoidCallback? onPressed;
final TripzButtonStyle style;
final bool loading;
final IconData? icon;
final bool fullWidth;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
final labelStyle = theme.textTheme.labelLarge;
final effectiveOnPressed = loading ? null : onPressed;
switch (style) {
case TripzButtonStyle.primary:
return SizedBox(
width: fullWidth ? double.infinity : null,
height: T.buttonHeight,
child: FilledButton(
onPressed: effectiveOnPressed,
child: _buildChild(cs.primary, labelStyle),
),
);
case TripzButtonStyle.secondary:
return SizedBox(
width: fullWidth ? double.infinity : null,
height: T.buttonHeight,
child: OutlinedButton(
onPressed: effectiveOnPressed,
child: _buildChild(cs.primary, labelStyle),
),
);
case TripzButtonStyle.text:
return TextButton(
onPressed: effectiveOnPressed,
child: _buildChild(cs.primary, labelStyle),
);
}
}
Widget _buildChild(Color color, TextStyle? style) {
if (loading) {
return const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(strokeWidth: 2),
);
}
if (icon != null) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 20),
const SizedBox(width: T.s8),
Text(label, style: style),
],
);
}
return Text(label, style: style);
}
}
+48
View File
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// بطاقة موحّدة: رحلة / سائق / محفظة — زوايا 12، حشوة 16.
/// docs/26 §7
class TripzCard extends StatelessWidget {
const TripzCard({
super.key,
required this.child,
this.padding,
this.onTap,
this.color,
this.borderColor,
});
final Widget child;
final EdgeInsetsGeometry? padding;
final VoidCallback? onTap;
final Color? color;
final Color? borderColor;
@override
Widget build(BuildContext context) {
final card = Card(
color: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(T.r12),
side: borderColor != null
? BorderSide(color: borderColor!)
: BorderSide.none,
),
child: Padding(
padding: padding ?? const EdgeInsets.all(T.pagePadding),
child: child,
),
);
if (onTap != null) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(T.r12),
child: card,
);
}
return card;
}
}
+156
View File
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// السكافولد الموحّد — كل شاشة في التطبيق تُبنى داخله.
/// يحوّل `status` من Cubit/Bloc إلى: skeleton / error / empty / محتوى.
/// docs/26 §6
class TripzScaffold extends StatelessWidget {
const TripzScaffold({
super.key,
this.title,
this.status,
this.onRetry,
this.empty,
this.bottomAction,
this.actions,
this.showBackButton = false,
this.onBack,
this.safeAreaTop = true,
this.safeAreaBottom = true,
this.padding = true,
this.backgroundColor,
required this.child,
});
final String? title;
final Enum? status;
final VoidCallback? onRetry;
final Widget? empty;
final Widget? bottomAction;
final List<Widget>? actions;
final bool showBackButton;
final VoidCallback? onBack;
final bool safeAreaTop;
final bool safeAreaBottom;
final bool padding;
final Color? backgroundColor;
final Widget child;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
return Scaffold(
backgroundColor: backgroundColor ?? cs.surface,
appBar: title != null
? AppBar(
title: Text(title!),
leading: showBackButton
? IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
onPressed: onBack ?? () => Navigator.maybePop(context),
)
: null,
actions: actions,
)
: null,
body: SafeArea(
top: safeAreaTop,
bottom: safeAreaBottom,
child: _buildBody(context),
),
bottomNavigationBar: bottomAction != null
? Padding(
padding: EdgeInsets.fromLTRB(
T.pagePadding,
0,
T.pagePadding,
MediaQuery.of(context).viewInsets.bottom > 0
? MediaQuery.of(context).viewInsets.bottom + 8
: T.pagePadding,
),
child: bottomAction,
)
: null,
);
}
Widget _buildBody(BuildContext context) {
if (status != null) {
if (_isError) return ErrorView(onRetry: onRetry);
if (_isLoading) return const Skeleton();
if (_isEmpty) return empty ?? const EmptyView();
}
return padding
? Padding(
padding: const EdgeInsets.all(T.pagePadding),
child: child,
)
: child;
}
bool get _isLoading =>
status != null && status!.name == 'loading';
bool get _isError =>
status != null && status!.name == 'error';
bool get _isEmpty =>
status != null && status!.name == 'empty';
}
class ErrorView extends StatelessWidget {
final VoidCallback? onRetry;
const ErrorView({super.key, this.onRetry});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(T.pagePadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline_rounded, size: 48, color: cs.error),
const SizedBox(height: T.s16),
Text(
'حدث خطأ',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
if (onRetry != null) ...[
const SizedBox(height: T.s16),
FilledButton(onPressed: onRetry, child: const Text('إعادة المحاولة')),
],
],
),
),
);
}
}
class Skeleton extends StatelessWidget {
const Skeleton({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}
class EmptyView extends StatelessWidget {
const EmptyView({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'لا توجد بيانات',
style: Theme.of(context).textTheme.bodyLarge,
),
);
}
}
+75
View File
@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// كل bottom sheet في التطبيق يمرّ من هنا: مقبض علوي، زوايا 24.
/// docs/26 §7
class TripzSheet extends StatelessWidget {
const TripzSheet({
super.key,
this.title,
this.child,
this.bottomAction,
this.isScrollControlled = true,
});
final String? title;
final Widget? child;
final Widget? bottomAction;
final bool isScrollControlled;
/// مساعد للاستدعاء بأمر واحد
static Future<T?> show<T>(
BuildContext context, {
String? title,
Widget? child,
Widget? bottomAction,
bool isScrollControlled = true,
}) {
return showModalBottomSheet<T>(
context: context,
isScrollControlled: isScrollControlled,
useSafeArea: true,
builder: (_) => TripzSheet(
title: title,
child: child,
bottomAction: bottomAction,
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
final bottomPadding = MediaQuery.of(context).viewInsets.bottom;
return Padding(
padding: EdgeInsets.fromLTRB(0, 0, 0, bottomPadding),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: T.pagePadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (title != null) ...[
Text(
title!,
style: theme.textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: T.s16),
],
if (child != null) child!,
if (bottomAction != null) ...[
const SizedBox(height: T.s16),
bottomAction!,
const SizedBox(height: T.s8),
],
],
),
),
),
);
}
}
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import '../design/tokens.dart';
/// حقل إدخال موحّد: هاتف · OTP · بحث — رسالة الخطأ تحته بالعربية.
/// docs/26 §7
class TripzTextField extends StatelessWidget {
const TripzTextField({
super.key,
this.controller,
this.label,
this.hint,
this.prefix,
this.suffix,
this.keyboardType,
this.obscureText = false,
this.errorText,
this.enabled = true,
this.onChanged,
this.onSubmitted,
this.autofocus = false,
this.textAlign,
this.maxLength,
this.contentPadding,
this.focusNode,
});
final TextEditingController? controller;
final String? label;
final String? hint;
final Widget? prefix;
final Widget? suffix;
final TextInputType? keyboardType;
final bool obscureText;
final String? errorText;
final bool enabled;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final bool autofocus;
final TextAlign? textAlign;
final int? maxLength;
final EdgeInsetsGeometry? contentPadding;
final FocusNode? focusNode;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: T.inputHeight,
child: TextField(
controller: controller,
focusNode: focusNode,
keyboardType: keyboardType,
obscureText: obscureText,
enabled: enabled,
onChanged: onChanged,
onSubmitted: onSubmitted,
autofocus: autofocus,
textAlign: textAlign ?? TextAlign.start,
maxLength: maxLength,
style: Theme.of(context).textTheme.bodyLarge,
decoration: InputDecoration(
labelText: label,
hintText: hint,
prefixIcon: prefix,
suffixIcon: suffix,
counterText: '',
contentPadding: contentPadding ??
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
),
),
if (errorText != null) ...[
const SizedBox(height: 4),
Text(
errorText!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: cs.error,
),
),
],
],
);
}
}