Update codebase
This commit is contained in:
@@ -23,11 +23,32 @@ class BuildConfig {
|
||||
static const String logoAsset = 'assets/images/logo.png';
|
||||
static const String splashLogoAsset = 'assets/images/splash_logo.png';
|
||||
|
||||
/// روابط خارجية لكل مستأجر — تُولَّد مع بقية الملف (N3).
|
||||
static const String storeUrl = String.fromEnvironment(
|
||||
'STORE_URL',
|
||||
defaultValue: 'https://tripz.app',
|
||||
/// روابط المتاجر الأربعة — **صفحات المتجر الحقيقية المنشورة**، أعطاها
|
||||
/// المالك 2026-08-05. لا تُستعمل مباشرةً: `StoreLinks` هو من يختار الرابط
|
||||
/// حسب المنصّة والدور، لأن «قيّم التطبيق» على آيفون سائق ≠ نفس الرابط على
|
||||
/// أندرويد راكب.
|
||||
///
|
||||
/// المعرّفات تطابق هوية النشر المقفلة: `com.mobileapp.store.ride` للراكب
|
||||
/// و`com.sefer_driver` للسائق.
|
||||
static const String riderIosUrl = String.fromEnvironment(
|
||||
'RIDER_IOS_URL',
|
||||
defaultValue: 'https://apps.apple.com/eg/app/tripz-rider/id6458734951',
|
||||
);
|
||||
static const String driverIosUrl = String.fromEnvironment(
|
||||
'DRIVER_IOS_URL',
|
||||
defaultValue: 'https://apps.apple.com/eg/app/tripz-driver/id6502189302',
|
||||
);
|
||||
static const String riderAndroidUrl = String.fromEnvironment(
|
||||
'RIDER_ANDROID_URL',
|
||||
defaultValue:
|
||||
'https://play.google.com/store/apps/details?id=com.mobileapp.store.ride',
|
||||
);
|
||||
static const String driverAndroidUrl = String.fromEnvironment(
|
||||
'DRIVER_ANDROID_URL',
|
||||
defaultValue:
|
||||
'https://play.google.com/store/apps/details?id=com.sefer_driver',
|
||||
);
|
||||
|
||||
static const String privacyUrl = String.fromEnvironment(
|
||||
'PRIVACY_URL',
|
||||
defaultValue: 'https://tripz.app/privacy',
|
||||
@@ -38,12 +59,6 @@ class BuildConfig {
|
||||
'SUPPORT_PHONE',
|
||||
defaultValue: '962790000000',
|
||||
);
|
||||
|
||||
/// رابط تطبيق الطرف الآخر — الراكب يدعو نفسه ليصير سائقاً والعكس.
|
||||
static const String driverAppUrl = String.fromEnvironment(
|
||||
'DRIVER_APP_URL',
|
||||
defaultValue: 'https://tripz.app/driver',
|
||||
);
|
||||
}
|
||||
|
||||
/// أعلام الاستحقاقات (docs/23 §5). **`const` إجبارياً** كي يُحذف كود الميزة
|
||||
|
||||
@@ -25,6 +25,8 @@ import 'api/antlaq_api.dart';
|
||||
import 'api/api_client.dart';
|
||||
import 'build_config.dart';
|
||||
import 'location/location_service.dart';
|
||||
import '../features/trip/data/recent_places_store.dart';
|
||||
import 'notifications/notification_store.dart';
|
||||
import 'notifications/push_service.dart';
|
||||
import 'realtime/realtime_service.dart';
|
||||
import 'session/session_cubit.dart';
|
||||
@@ -79,7 +81,12 @@ Future<void> setupInjector() async {
|
||||
sl.registerSingleton<RealtimeService>(RealtimeService(tokens));
|
||||
|
||||
sl.registerSingleton<TenantCubit>(TenantCubit(sl(), prefs));
|
||||
sl.registerSingleton<PushService>(PushService(sl()));
|
||||
|
||||
// الصندوق يُحمَّل قبل `PushService` كي لا يضيع إشعارٌ يصل أثناء الإقلاع.
|
||||
final notifications = NotificationStore();
|
||||
await notifications.load();
|
||||
sl.registerSingleton<NotificationStore>(notifications);
|
||||
sl.registerSingleton<PushService>(PushService(sl(), notifications));
|
||||
|
||||
sl.registerSingleton<HistoryRepository>(HistoryRepository(sl()));
|
||||
sl.registerFactory<HistoryCubit>(() => HistoryCubit(sl()));
|
||||
@@ -105,11 +112,18 @@ Future<void> setupInjector() async {
|
||||
sl.registerFactory<WalletCubit>(() => WalletCubit(sl()));
|
||||
}
|
||||
|
||||
// الأماكن الأخيرة تُحمَّل مرة واحدة عند الإقلاع: الشاشة الرئيسية تقرأها
|
||||
// في أول إطار، وقراءةُ القرص وقتها تعني صفّاً يقفز بعد الرسم.
|
||||
final recents = RecentPlacesStore();
|
||||
await recents.load();
|
||||
sl.registerSingleton<RecentPlacesStore>(recents);
|
||||
|
||||
sl.registerFactory<RideCubit>(() => RideCubit(
|
||||
trips: sl(),
|
||||
maps: sl(),
|
||||
location: sl(),
|
||||
realtime: sl(),
|
||||
recents: sl(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@
|
||||
"settingsLanguage": "اللغة",
|
||||
"settingsLanguageArabic": "العربية",
|
||||
"settingsLanguageEnglish": "English",
|
||||
"settingsSectionGeneral": "عام",
|
||||
"settingsSectionSupport": "الدعم والمعلومات",
|
||||
"settingsSupportLead": "أسئلة شائعة وطرق التواصل",
|
||||
"settingsShareLead": "شارك التطبيق مع أصدقائك",
|
||||
"rideWhereTo": "إلى أين؟",
|
||||
"rideOrigin": "من",
|
||||
"rideDestination": "إلى",
|
||||
@@ -131,6 +135,8 @@
|
||||
"rateSubmit": "إرسال التقييم",
|
||||
"rateSkip": "لاحقاً",
|
||||
"rateAlready": "سبق أن قيّمت هذه الرحلة",
|
||||
"tripCall": "اتصال",
|
||||
"tripMessage": "رسالة",
|
||||
"tripFareTitle": "تفاصيل الأجرة",
|
||||
"tripFare": "الأجرة",
|
||||
"tripEarnings": "أرباحك",
|
||||
@@ -169,9 +175,14 @@
|
||||
"menuEarnings": "أرباحي",
|
||||
"menuSettings": "الإعدادات",
|
||||
"menuSupport": "المساعدة",
|
||||
"menuNotifications": "الإشعارات",
|
||||
"notificationsEmpty": "لا إشعارات بعد — سنُعلمك هنا بكل ما يخصّ رحلاتك",
|
||||
"notificationsClear": "مسح الكل",
|
||||
"menuAbout": "عن التطبيق",
|
||||
"menuLogout": "تسجيل الخروج",
|
||||
"menuLogoutConfirm": "هل تريد الخروج من حسابك؟",
|
||||
"paymentCash": "نقداً",
|
||||
"paymentWallet": "من المحفظة",
|
||||
"historyTitle": "رحلاتي",
|
||||
"historyEmpty": "لا رحلات بعد — اطلب أول رحلة لك",
|
||||
"historyStatusCompleted": "مكتملة",
|
||||
@@ -224,12 +235,24 @@
|
||||
"menuNotifications": "الإشعارات",
|
||||
"menuPrivacy": "سياسة الخصوصية",
|
||||
"menuRateApp": "قيّم التطبيق",
|
||||
"shareMessage": "جرّب تطبيق Tripz — استعمل كودي {code} واحصل على خصم أول رحلة",
|
||||
"shareMessage": "جرّب تطبيق Tripz — استعمل كودي {code} واحصل على خصم أول رحلة\n{link}",
|
||||
"shareMessagePlain": "جرّب تطبيق Tripz\n{link}",
|
||||
"menuRiderApp": "حمّل تطبيق الراكب",
|
||||
"menuDriverApp": "سجّل كسائق",
|
||||
"@shareMessage": {
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"type": "String"
|
||||
},
|
||||
"link": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@shareMessagePlain": {
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -65,6 +65,10 @@
|
||||
"settingsLanguage": "Language",
|
||||
"settingsLanguageArabic": "العربية",
|
||||
"settingsLanguageEnglish": "English",
|
||||
"settingsSectionGeneral": "General",
|
||||
"settingsSectionSupport": "Support & info",
|
||||
"settingsSupportLead": "FAQ and ways to reach us",
|
||||
"settingsShareLead": "Share the app with your friends",
|
||||
"rideWhereTo": "Where to?",
|
||||
"rideOrigin": "From",
|
||||
"rideDestination": "To",
|
||||
@@ -131,6 +135,8 @@
|
||||
"rateSubmit": "Submit rating",
|
||||
"rateSkip": "Later",
|
||||
"rateAlready": "You already rated this trip",
|
||||
"tripCall": "Call",
|
||||
"tripMessage": "Message",
|
||||
"tripFareTitle": "Fare breakdown",
|
||||
"tripFare": "Fare",
|
||||
"tripEarnings": "Your earnings",
|
||||
@@ -169,9 +175,14 @@
|
||||
"menuEarnings": "Earnings",
|
||||
"menuSettings": "Settings",
|
||||
"menuSupport": "Help",
|
||||
"menuNotifications": "Notifications",
|
||||
"notificationsEmpty": "No notifications yet — we'll keep you posted about your trips here",
|
||||
"notificationsClear": "Clear all",
|
||||
"menuAbout": "About",
|
||||
"menuLogout": "Sign out",
|
||||
"menuLogoutConfirm": "Sign out of your account?",
|
||||
"paymentCash": "Cash",
|
||||
"paymentWallet": "Wallet",
|
||||
"historyTitle": "My trips",
|
||||
"historyEmpty": "No trips yet — request your first ride",
|
||||
"historyStatusCompleted": "Completed",
|
||||
@@ -224,12 +235,24 @@
|
||||
"menuNotifications": "Notifications",
|
||||
"menuPrivacy": "Privacy policy",
|
||||
"menuRateApp": "Rate the app",
|
||||
"shareMessage": "Try Tripz — use my code {code} for a discount on your first ride",
|
||||
"shareMessage": "Try Tripz — use my code {code} for a discount on your first ride\n{link}",
|
||||
"shareMessagePlain": "Try Tripz\n{link}",
|
||||
"menuRiderApp": "Get the rider app",
|
||||
"menuDriverApp": "Become a driver",
|
||||
"@shareMessage": {
|
||||
"placeholders": {
|
||||
"code": {
|
||||
"type": "String"
|
||||
},
|
||||
"link": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@shareMessagePlain": {
|
||||
"placeholders": {
|
||||
"link": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// إشعار وصل إلى الجهاز — يُحفظ محلياً لأن الخادم لا يسرد شيئاً.
|
||||
class AppNotification {
|
||||
const AppNotification({
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.receivedAt,
|
||||
this.read = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
final DateTime receivedAt;
|
||||
final bool read;
|
||||
|
||||
AppNotification copyWith({bool? read}) => AppNotification(
|
||||
title: title,
|
||||
body: body,
|
||||
receivedAt: receivedAt,
|
||||
read: read ?? this.read,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
't': title,
|
||||
'b': body,
|
||||
'at': receivedAt.toIso8601String(),
|
||||
'r': read,
|
||||
};
|
||||
|
||||
factory AppNotification.fromJson(Map<String, dynamic> json) =>
|
||||
AppNotification(
|
||||
title: json['t'] as String? ?? '',
|
||||
body: json['b'] as String? ?? '',
|
||||
receivedAt:
|
||||
DateTime.tryParse(json['at']?.toString() ?? '') ?? DateTime.now(),
|
||||
read: json['r'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// صندوق الإشعارات — **محلّي بالكامل وعن قصد**.
|
||||
///
|
||||
/// الخادم يوفّر `POST /notifications/token` فقط ولا نقطة سرد
|
||||
/// (`backend-archive/src/modules/notifications`, docs/38 §11). فبدل شاشة
|
||||
/// فارغة تعتمد على نقطة غير موجودة، نحفظ ما يصل فعلاً عبر FCM بينما التطبيق
|
||||
/// يعمل. سيرو يعرض `NotificationPage` في الدراور
|
||||
/// (`map_menu_widget.dart:354`) — هذا مقابلها الصادق.
|
||||
///
|
||||
/// **حدّ معروف:** ما يصل والتطبيق مغلق تماماً لا يُلتقط — لا `background
|
||||
/// handler` مسجَّلاً. حين تُضاف نقطة سرد على الخادم، يُستبدل هذا المصدر
|
||||
/// بها ويبقى نفس عرض الشاشة.
|
||||
class NotificationStore {
|
||||
static const _key = 'notifications_inbox';
|
||||
|
||||
/// سقف الاحتفاظ — صندوق بلا حدّ يتضخّم بلا فائدة.
|
||||
static const _max = 50;
|
||||
|
||||
List<AppNotification> _items = const [];
|
||||
List<AppNotification> get items => _items;
|
||||
|
||||
int get unreadCount => _items.where((n) => !n.read).length;
|
||||
|
||||
Future<void> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_key);
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
try {
|
||||
final list = jsonDecode(raw) as List<dynamic>;
|
||||
_items = list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(AppNotification.fromJson)
|
||||
.toList(growable: false);
|
||||
} catch (_) {
|
||||
// تخزين تالف لا يمنع فتح الشاشة — يُهمل ويُكتب فوقه عند أول إشعار.
|
||||
_items = const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> add(AppNotification notification) async {
|
||||
_items = [notification, ..._items].take(_max).toList(growable: false);
|
||||
await _persist();
|
||||
}
|
||||
|
||||
Future<void> markAllRead() async {
|
||||
if (_items.every((n) => n.read)) return;
|
||||
_items = _items.map((n) => n.copyWith(read: true)).toList(growable: false);
|
||||
await _persist();
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
_items = const [];
|
||||
await _persist();
|
||||
}
|
||||
|
||||
Future<void> _persist() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_key,
|
||||
jsonEncode(_items.map((n) => n.toJson()).toList(growable: false)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,23 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../api/api_exception.dart';
|
||||
import 'notification_store.dart';
|
||||
|
||||
/// إشعارات FCM — **الطبقة الوحيدة التي تعرف Firebase**.
|
||||
///
|
||||
/// عقد الخادم نقطة واحدة: `POST /notifications/token { token, platform }`
|
||||
/// (docs/38 §11). لا نقطة سرد إشعارات، فلا شاشة «صندوق إشعارات» تُبنى على
|
||||
/// وهم — ما يصل يصل من النظام.
|
||||
/// (docs/38 §11). لا نقطة **سرد** إشعارات، فالصندوق محلّي: ما يصل أثناء عمل
|
||||
/// التطبيق يُحفظ في [NotificationStore] ويُعرض في `NotificationsPage` —
|
||||
/// انظر تعليق ذلك الصنف لحدود هذا الاختيار.
|
||||
///
|
||||
/// الإعداد الأصلي جاهز مسبقاً: `google-services.json` و
|
||||
/// `GoogleService-Info.plist` منقولان مع الطبقة الأصلية، و`build.gradle`
|
||||
/// يطبّق `com.google.gms.google-services` أصلاً.
|
||||
class PushService {
|
||||
PushService(this._api);
|
||||
PushService(this._api, this._store);
|
||||
|
||||
final ApiClient _api;
|
||||
final NotificationStore _store;
|
||||
|
||||
/// يُنادى **بعد** الدخول: التسجيل يربط الجهاز بالمستخدم، فقبل الجلسة
|
||||
/// لا معنى له.
|
||||
@@ -35,11 +38,30 @@ class PushService {
|
||||
// الرمز يُدوَّر من Google دورياً — بلا هذا الاشتراك تتوقّف الإشعارات
|
||||
// بصمت بعد أسابيع.
|
||||
messaging.onTokenRefresh.listen(_send);
|
||||
|
||||
// ما يصل والتطبيق مفتوح، وما فُتح التطبيق بالضغط عليه.
|
||||
FirebaseMessaging.onMessage.listen(_capture);
|
||||
FirebaseMessaging.onMessageOpenedApp.listen(_capture);
|
||||
final initial = await messaging.getInitialMessage();
|
||||
if (initial != null) await _capture(initial);
|
||||
} catch (_) {
|
||||
// غياب الإشعارات لا يمنع استعمال التطبيق — لا يُسقط الإقلاع.
|
||||
}
|
||||
}
|
||||
|
||||
/// إشعار بلا عنوان ولا نصّ (رسالة بيانات صرفة) لا يُعرض — سطر فارغ في
|
||||
/// الصندوق أسوأ من لا شيء.
|
||||
Future<void> _capture(RemoteMessage message) async {
|
||||
final title = message.notification?.title ?? '';
|
||||
final body = message.notification?.body ?? '';
|
||||
if (title.isEmpty && body.isEmpty) return;
|
||||
await _store.add(AppNotification(
|
||||
title: title,
|
||||
body: body,
|
||||
receivedAt: message.sentTime ?? DateTime.now(),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _send(String token) async {
|
||||
try {
|
||||
await _api.post<Map<String, dynamic>>(
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../features/chat/view/chat_page.dart';
|
||||
import '../features/profile/view/profile_edit_page.dart';
|
||||
import '../features/profile/view/profile_setup_page.dart';
|
||||
import '../features/rating/data/rating_repository.dart';
|
||||
import '../features/notifications/view/notifications_page.dart';
|
||||
import '../features/rewards/view/rewards_page.dart';
|
||||
import '../features/rating/view/rating_page.dart';
|
||||
import '../features/settings/view/settings_page.dart';
|
||||
@@ -34,6 +35,7 @@ class Routes {
|
||||
static const support = '/support';
|
||||
static const rewards = '/rewards';
|
||||
static const chat = '/chat';
|
||||
static const notifications = '/notifications';
|
||||
}
|
||||
|
||||
/// التوجيه يُشتقّ من `SessionCubit` وحده — لا `Navigator.push` بعد الدخول
|
||||
@@ -120,6 +122,10 @@ final appRouter = GoRouter(
|
||||
path: Routes.support,
|
||||
builder: (context, state) => const SupportPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.notifications,
|
||||
builder: (context, state) => const NotificationsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.home,
|
||||
builder: (context, state) => const RidePage(),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'build_config.dart';
|
||||
import 'config.dart';
|
||||
|
||||
/// اختيار رابط المتجر الصحيح — **بالمنصّة والدور معاً**.
|
||||
///
|
||||
/// كان الخطأ (مثبت 2026-08-05): رابط واحد ثابت `https://tripz.app` لكل
|
||||
/// شيء — فـ«قيّم التطبيق» لا يفتح صفحة تقييم، و«شارك التطبيق» يرسل رسالة
|
||||
/// بلا رابط أصلاً، و«سجّل كسائق» يفتح صفحة تسويقية لا تطبيق السائق.
|
||||
///
|
||||
/// أربع حالات لا واحدة: {آيفون، أندرويد} × {راكب، سائق}. والدور يأتي من
|
||||
/// [AppConfig.appRole] الذي يميّز البناءين أصلاً.
|
||||
class StoreLinks {
|
||||
const StoreLinks._();
|
||||
|
||||
static bool get _isIos => defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
static bool get _isDriverBuild => AppConfig.appRole == 'driver';
|
||||
|
||||
/// رابط تطبيق الراكب على منصّة هذا الجهاز.
|
||||
static String get rider =>
|
||||
_isIos ? BuildConfig.riderIosUrl : BuildConfig.riderAndroidUrl;
|
||||
|
||||
/// رابط تطبيق السائق على منصّة هذا الجهاز.
|
||||
static String get driver =>
|
||||
_isIos ? BuildConfig.driverIosUrl : BuildConfig.driverAndroidUrl;
|
||||
|
||||
/// **هذا** التطبيق في متجر هذا الجهاز — لـ«قيّم التطبيق» ولرابط المشاركة.
|
||||
///
|
||||
/// السائق يشارك رابط السائق، والراكب رابط الراكب: مشاركةُ راكبٍ لرابط
|
||||
/// السائق تُنزِّل المدعوَّ تطبيقاً خاطئاً.
|
||||
static String get thisApp => _isDriverBuild ? driver : rider;
|
||||
|
||||
/// تطبيق الطرف الآخر — «سجّل كسائق» في تطبيق الراكب، والعكس.
|
||||
static String get otherApp => _isDriverBuild ? rider : driver;
|
||||
}
|
||||
@@ -8,10 +8,13 @@ import '../build_config.dart';
|
||||
import '../config.dart';
|
||||
import '../design/tokens.dart';
|
||||
import '../design/tripz_colors.dart';
|
||||
import '../di.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../notifications/notification_store.dart';
|
||||
import '../router.dart';
|
||||
import '../session/session_cubit.dart';
|
||||
import '../session/session_state.dart';
|
||||
import '../store_links.dart';
|
||||
import '../tenant/feature_gate.dart';
|
||||
|
||||
/// الدراور — نقطة تطبيق **طبقتَي الميزات** معاً (docs/23 §5 + docs/38 §10):
|
||||
@@ -43,6 +46,14 @@ class AppDrawer extends StatelessWidget {
|
||||
label: l10n.menuProfile,
|
||||
route: Routes.profileEdit,
|
||||
),
|
||||
_Item(
|
||||
icon: Icons.notifications_none_rounded,
|
||||
label: l10n.menuNotifications,
|
||||
route: Routes.notifications,
|
||||
// العدّاد يُقرأ عند بناء الدراور — والدراور يُبنى عند كل
|
||||
// فتح، فلا حاجة لتيّار يراقب مخزناً في الذاكرة.
|
||||
badge: sl<NotificationStore>().unreadCount,
|
||||
),
|
||||
_Item(
|
||||
icon: Icons.route_outlined,
|
||||
label: l10n.menuTrips,
|
||||
@@ -72,28 +83,36 @@ class AppDrawer extends StatelessWidget {
|
||||
route: Routes.support,
|
||||
),
|
||||
// مداخل محليّة بلا نقطة خادم — تُنفَّذ في الجهاز مباشرة.
|
||||
//
|
||||
// الروابط من `StoreLinks` لا من ثابتٍ واحد: هذا البناء
|
||||
// يشارك **رابط نفسه** (سائق يشارك رابط السائق)، ويفتح
|
||||
// متجر منصّته هو.
|
||||
_Action(
|
||||
icon: Icons.share_outlined,
|
||||
label: l10n.menuShare,
|
||||
onTap: () => SharePlus.instance
|
||||
.share(ShareParams(text: l10n.shareMessage(''))),
|
||||
onTap: () => SharePlus.instance.share(
|
||||
ShareParams(
|
||||
text: l10n.shareMessagePlain(StoreLinks.thisApp),
|
||||
),
|
||||
),
|
||||
),
|
||||
_Action(
|
||||
icon: Icons.star_outline_rounded,
|
||||
label: l10n.menuRateApp,
|
||||
onTap: () => _open(BuildConfig.storeUrl),
|
||||
onTap: () => _open(StoreLinks.thisApp),
|
||||
),
|
||||
_Action(
|
||||
icon: Icons.shield_outlined,
|
||||
label: l10n.menuPrivacy,
|
||||
onTap: () => _open(BuildConfig.privacyUrl),
|
||||
),
|
||||
if (!isDriver)
|
||||
_Action(
|
||||
icon: Icons.local_taxi_outlined,
|
||||
label: l10n.menuDriverApp,
|
||||
onTap: () => _open(BuildConfig.driverAppUrl),
|
||||
),
|
||||
_Action(
|
||||
icon: isDriver
|
||||
? Icons.person_outline_rounded
|
||||
: Icons.local_taxi_outlined,
|
||||
label: isDriver ? l10n.menuRiderApp : l10n.menuDriverApp,
|
||||
onTap: () => _open(StoreLinks.otherApp),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -220,17 +239,43 @@ class _Action extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Item extends StatelessWidget {
|
||||
const _Item({required this.icon, required this.label, required this.route});
|
||||
const _Item({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.route,
|
||||
this.badge = 0,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String route;
|
||||
|
||||
/// عدّاد غير المقروء — يُخفى عند الصفر.
|
||||
final int badge;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
trailing: badge == 0
|
||||
? null
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Space.xs,
|
||||
vertical: Space.xxs / 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.tripzColors.danger,
|
||||
borderRadius: Radii.field_,
|
||||
),
|
||||
child: Text(
|
||||
'$badge',
|
||||
style: context.texts.labelSmall?.copyWith(
|
||||
color: context.colors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
context.push(route);
|
||||
|
||||
Reference in New Issue
Block a user