Update codebase
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tripz_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
|
||||
import 'mydialoug.dart';
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
|
||||
|
||||
/// DriverEarningsBadge
|
||||
/// ─────────────────────
|
||||
/// ويدجت يعرض للسائق مقارنة أرباحه مع تريبز مقارنةً بالمنافسين.
|
||||
/// ويدجت يعرض للسائق مقارنة أرباحه مع سيرو مقارنةً بالمنافسين.
|
||||
/// يُستخدم في شاشة قبول الطلب ليحفّز السائق على القبول.
|
||||
class DriverEarningsBadge extends StatelessWidget {
|
||||
/// النص الكامل الجاهز من السيرفر
|
||||
final String? earningsLabel;
|
||||
|
||||
/// المبلغ الإضافي الذي يكسبه السائق (موجب = أكثر مع تريبز)
|
||||
/// المبلغ الإضافي الذي يكسبه السائق (موجب = أكثر مع سيرو)
|
||||
final double? extraAmount;
|
||||
|
||||
/// رمز العملة
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tripz_driver/constant/style.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tripz_driver/constant/style.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:tripz_driver/constant/style.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:tripz_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
|
||||
class MyTextForm extends StatelessWidget {
|
||||
const MyTextForm({
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tripz_driver/constant/colors.dart';
|
||||
import 'package:tripz_driver/constant/style.dart';
|
||||
import 'package:tripz_driver/controller/functions/tts.dart';
|
||||
import 'package:tripz_driver/controller/functions/translate_helper.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/tts.dart';
|
||||
import 'package:siro_driver/controller/functions/translate_helper.dart';
|
||||
|
||||
class DialogConfig {
|
||||
static const Duration animationDuration = Duration(milliseconds: 200);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
/// شارة نوع الطلب: خاص أم عام.
|
||||
///
|
||||
/// الخادم يمنح السائق الأفضل ترتيباً سبقاً حصرياً بضع ثوانٍ قبل أن يُطلق
|
||||
/// الطلب للجميع (DISPATCH_MODE=batched). بدون هذه الشارة يبدو الفرق
|
||||
/// عشوائياً للسائق؛ معها يفهم أنه صاحب الأولوية الآن وأن التأخير يكلّفه
|
||||
/// الطلب.
|
||||
///
|
||||
/// المصدر: حقل `offer_type` في حمولة السوكِت — 'exclusive' أو 'public'.
|
||||
/// غيابه يعني طلباً عاماً (كل المسارات القديمة والبثّ الحر)، فلا تظهر
|
||||
/// الشارة إطلاقاً في وضع البثّ العادي.
|
||||
class OfferTypeBadge extends StatelessWidget {
|
||||
final Map rideInfo;
|
||||
|
||||
const OfferTypeBadge({Key? key, required this.rideInfo}) : super(key: key);
|
||||
|
||||
bool get _isExclusive =>
|
||||
rideInfo['offer_type']?.toString() == 'exclusive';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// لا شارة في الوضع العادي — لا نضيف ضجيجاً بصرياً بلا معنى.
|
||||
if (rideInfo['offer_type'] == null) return const SizedBox.shrink();
|
||||
|
||||
final Color color = _isExclusive ? Colors.amber.shade800 : Colors.blueGrey;
|
||||
final IconData icon = _isExclusive ? Icons.star_rounded : Icons.public;
|
||||
final String label = _isExclusive ? 'طلب خاص لك'.tr : 'طلب عام'.tr;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
/// شارة «رحلة مجدولة».
|
||||
///
|
||||
/// السائق يجب أن يعرف أن هذه ليست رحلة لحظية بل موعد مضبوط: الراكب
|
||||
/// ينتظره في وقت محدد، والتأخر عليه يختلف عن التأخر على طلب فوري.
|
||||
///
|
||||
/// المصدر: حقلا `is_scheduled` و`scheduled_at` في حمولة الرحلة —
|
||||
/// يضيفهما buildMarketPayload في backend/ride/rides/add_ride.php.
|
||||
/// غيابهما = رحلة عادية، فلا تظهر الشارة إطلاقاً.
|
||||
///
|
||||
/// بخلاف شارة «طلب خاص/عام» التي تظهر في نافذة العرض فقط، هذه تظهر في
|
||||
/// **كل** السطوح: القائمة، ونافذة الطلب، والأوفرلي — لأن القيمة هنا
|
||||
/// معلومة دائمة عن الرحلة لا حالة مؤقتة تنقضي بعد ثوانٍ.
|
||||
class ScheduledRideBadge extends StatelessWidget {
|
||||
final Map rideInfo;
|
||||
|
||||
/// حجم مصغّر لبطاقات القائمة.
|
||||
final bool compact;
|
||||
|
||||
const ScheduledRideBadge({
|
||||
super.key,
|
||||
required this.rideInfo,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
static bool isScheduled(Map? info) {
|
||||
final v = info?['is_scheduled']?.toString();
|
||||
return v == '1' || v == 'true';
|
||||
}
|
||||
|
||||
/// وقت الموعد بصيغة قصيرة. يرجع فارغاً إن لم يصل أو تعذّر تحليله —
|
||||
/// الشارة تبقى ظاهرة بلا وقت بدل أن تختفي.
|
||||
String get _timeLabel {
|
||||
final raw = rideInfo['scheduled_at']?.toString();
|
||||
if (raw == null || raw.isEmpty) return '';
|
||||
|
||||
final dt = DateTime.tryParse(raw);
|
||||
if (dt == null) return '';
|
||||
|
||||
final now = DateTime.now();
|
||||
final sameDay =
|
||||
dt.year == now.year && dt.month == now.month && dt.day == now.day;
|
||||
|
||||
final hh = dt.hour.toString().padLeft(2, '0');
|
||||
final mm = dt.minute.toString().padLeft(2, '0');
|
||||
|
||||
if (sameDay) return '$hh:$mm';
|
||||
return '${dt.day}/${dt.month} $hh:$mm';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isScheduled(rideInfo)) return const SizedBox.shrink();
|
||||
|
||||
const color = Color(0xFF6A5ACD); // بنفسجي — لا يشبه ألوان الحالات الأخرى
|
||||
final t = _timeLabel;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: compact ? 8 : 12,
|
||||
vertical: compact ? 4 : 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(compact ? 12 : 20),
|
||||
border: Border.all(color: color.withOpacity(0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.schedule, color: color, size: compact ? 14 : 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
t.isEmpty ? 'رحلة مجدولة'.tr : '${'رحلة مجدولة'.tr} · $t',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: compact ? 11 : 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
/// شارة «راكب بلا إنترنت».
|
||||
///
|
||||
/// الراكب طلب رحلته برسالة نصية إلى رقم الشركة، لا من التطبيق. وهذا
|
||||
/// يقلب طريقة التعامل معه رأساً على عقب:
|
||||
///
|
||||
/// • لن يصله شيء في التطبيق — لا محادثة ولا إشعار «وصل السائق»
|
||||
/// • لن يرى السائق يقترب على الخريطة، فلا يعرف أين صار
|
||||
/// • لن يستطيع تعديل وجهته ولا إلغاءها من التطبيق
|
||||
///
|
||||
/// السائق الذي لا يعرف هذا سيرسل رسالة داخل التطبيق، ينتظر رداً لن
|
||||
/// يأتي، ثم يُلغي ظناً أن الراكب غير جاد. لذلك الشارة تصرّح بالمطلوب
|
||||
/// صراحةً — «اتصل به» — لا تكتفي بوصف الحالة.
|
||||
///
|
||||
/// المصدر: حقل `is_sms` في حمولة الرحلة، يضيفه buildMarketPayload في
|
||||
/// backend/ride/rides/add_ride.php. غيابه = رحلة عادية بلا شارة.
|
||||
class SmsRideBadge extends StatelessWidget {
|
||||
final Map rideInfo;
|
||||
|
||||
/// حجم مصغّر لبطاقات القائمة.
|
||||
final bool compact;
|
||||
|
||||
const SmsRideBadge({
|
||||
super.key,
|
||||
required this.rideInfo,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
static bool isSmsRide(Map? info) {
|
||||
final v = info?['is_sms']?.toString();
|
||||
return v == '1' || v == 'true';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isSmsRide(rideInfo)) return const SizedBox.shrink();
|
||||
|
||||
// برتقالي محذّر — يفترق عن بنفسجي «المجدولة» بوضوح، فالشارتان قد
|
||||
// تظهران متجاورتين ولا يجوز أن تلتبسا.
|
||||
const color = Color(0xFFE07B39);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: compact ? 8 : 12,
|
||||
vertical: compact ? 4 : 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(compact ? 12 : 20),
|
||||
border: Border.all(color: color.withOpacity(0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.phone_in_talk, color: color, size: compact ? 14 : 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
compact ? 'No internet — call'.tr : 'Passenger offline — call them'.tr,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: compact ? 11 : 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
|
||||
/// عدّة الواجهة الموحّدة لتطبيق السائق.
|
||||
///
|
||||
/// كل صفحات القائمة الجانبية تُبنى من هذه القطع، فالشكل واحد والثيم الليلي
|
||||
/// والنهاري يتبعان `Theme.of(context)` بلا ألوان مثبّتة داخل الصفحات.
|
||||
///
|
||||
/// **لا تضع منطقاً هنا** — القطع كلها عرض صرف بلا حالة ولا كنترولرات.
|
||||
class SiroUi {
|
||||
const SiroUi._();
|
||||
|
||||
// --- المسافات والانحناءات (مقياس واحد لكل التطبيق) ---
|
||||
static const double gap = 12;
|
||||
static const double pagePadding = 16;
|
||||
static const double cardRadius = 18;
|
||||
static const double controlRadius = 14;
|
||||
|
||||
static const EdgeInsets pageInsets =
|
||||
EdgeInsets.symmetric(horizontal: pagePadding, vertical: 12);
|
||||
|
||||
/// لون العنصر التفاعلي حسب الوضع: النيفي يختفي على الخلفية الداكنة.
|
||||
static Color interactive(BuildContext context) =>
|
||||
Theme.of(context).colorScheme.primary;
|
||||
|
||||
static Color subtle(BuildContext context) =>
|
||||
Theme.of(context).textTheme.bodySmall?.color ?? AppColor.grayColor;
|
||||
|
||||
static Color border(BuildContext context) => Theme.of(context).dividerColor;
|
||||
|
||||
static Color surface(BuildContext context) => Theme.of(context).cardColor;
|
||||
|
||||
/// تدرّج العلامة — يُستعمل في الرؤوس والبطاقات البارزة فقط، لا كخلفية صفحة.
|
||||
static LinearGradient brandGradient(BuildContext context) {
|
||||
final dark = Theme.of(context).brightness == Brightness.dark;
|
||||
return LinearGradient(
|
||||
colors: dark
|
||||
? const [Color(0xFF1E2745), Color(0xFF2E3A63)]
|
||||
: const [AppColor.primaryColor, Color(0xFF3A4674)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// الهيكل الموحّد لكل صفحة داخلية.
|
||||
///
|
||||
/// يستبدل الخليط الحالي: `Scaffold` عادي و`CupertinoPageScaffold` و`SliverAppBar`
|
||||
/// وخلفيات `Colors.white` المثبّتة (التي كانت تُنتج صفحة بيضاء بنص أبيض في
|
||||
/// الوضع الليلي — كما في سجل الرحلات).
|
||||
class SiroPage extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget body;
|
||||
final List<Widget>? actions;
|
||||
final Widget? floatingActionButton;
|
||||
final Widget? bottomBar;
|
||||
final PreferredSizeWidget? appBarBottom;
|
||||
final bool showBack;
|
||||
final Future<void> Function()? onRefresh;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const SiroPage({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.actions,
|
||||
this.floatingActionButton,
|
||||
this.bottomBar,
|
||||
this.appBarBottom,
|
||||
this.showBack = true,
|
||||
this.onRefresh,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content = padding == null
|
||||
? body
|
||||
: Padding(padding: padding!, child: body);
|
||||
|
||||
if (onRefresh != null) {
|
||||
content = RefreshIndicator(
|
||||
onRefresh: onRefresh!,
|
||||
color: SiroUi.interactive(context),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title, overflow: TextOverflow.ellipsis),
|
||||
automaticallyImplyLeading: showBack,
|
||||
actions: actions,
|
||||
bottom: appBarBottom,
|
||||
),
|
||||
body: SafeArea(child: content),
|
||||
floatingActionButton: floatingActionButton,
|
||||
bottomNavigationBar: bottomBar == null
|
||||
? null
|
||||
: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||
child: bottomBar,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// البطاقة الموحّدة: سطح + حدّ رفيع + انحناء واحد. بلا ظلال ثقيلة.
|
||||
class SiroCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final VoidCallback? onTap;
|
||||
final Color? accent;
|
||||
|
||||
const SiroCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
this.margin,
|
||||
this.onTap,
|
||||
this.accent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = BorderRadius.circular(SiroUi.cardRadius);
|
||||
return Padding(
|
||||
padding: margin ?? const EdgeInsets.only(bottom: SiroUi.gap),
|
||||
child: Material(
|
||||
color: SiroUi.surface(context),
|
||||
borderRadius: radius,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: radius,
|
||||
border: Border.all(
|
||||
color: accent?.withValues(alpha: 0.35) ?? SiroUi.border(context),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// عنوان قسم داخل الصفحة، بخط جانبي بلون العلامة.
|
||||
class SiroSectionTitle extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget? trailing;
|
||||
|
||||
const SiroSectionTitle(this.title, {super.key, this.trailing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: SiroUi.interactive(context),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// أيقونة داخل دائرة ملوّنة خفيفة — نفس نمط أيقونات القائمة الجانبية.
|
||||
class SiroIconBadge extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final double size;
|
||||
|
||||
const SiroIconBadge(this.icon,
|
||||
{super.key, required this.color, this.size = 20});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(size * 0.45),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: color, size: size),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// شارة نصية صغيرة (حالة رحلة، نوع عرض، ...).
|
||||
class SiroTag extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
final IconData? icon;
|
||||
|
||||
const SiroTag(this.label, {super.key, required this.color, this.icon});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 13, color: color),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// خانة إحصاء (قيمة كبيرة + تسمية) — تُستعمل في الأرباح والمحفظة والملف.
|
||||
class SiroStat extends StatelessWidget {
|
||||
final String value;
|
||||
final String label;
|
||||
final IconData? icon;
|
||||
final Color? color;
|
||||
|
||||
const SiroStat({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.label,
|
||||
this.icon,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = color ?? SiroUi.interactive(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
SiroIconBadge(icon!, color: c, size: 18),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: c,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// صفّ إحصاءات متساوية داخل بطاقة واحدة، بفواصل رأسية.
|
||||
class SiroStatRow extends StatelessWidget {
|
||||
final List<SiroStat> stats;
|
||||
|
||||
const SiroStatRow(this.stats, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < stats.length; i++) {
|
||||
if (i > 0) {
|
||||
children.add(Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: SiroUi.border(context),
|
||||
));
|
||||
}
|
||||
children.add(Expanded(child: stats[i]));
|
||||
}
|
||||
return SiroCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// حالة الفراغ. **منفصلة عمداً عن [SiroErrorState]**: عرض «لا يوجد» عند فشل
|
||||
/// الشبكة يُضلّل الكابتن ويجعله يظن أن لا عمل لديه.
|
||||
class SiroEmptyState extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String? message;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const SiroEmptyState({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.message,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(22),
|
||||
decoration: BoxDecoration(
|
||||
color: SiroUi.interactive(context).withValues(alpha: 0.08),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 40, color: SiroUi.interactive(context)),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
if (onAction != null && actionLabel != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
OutlinedButton(onPressed: onAction, child: Text(actionLabel!)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// حالة الخطأ — بلون تحذيري وبزر إعادة محاولة صريح.
|
||||
class SiroErrorState extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const SiroErrorState({super.key, required this.message, this.onRetry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(22),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor.withValues(alpha: 0.10),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.cloud_off_rounded,
|
||||
size: 40, color: AppColor.redColor),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 18),
|
||||
label: Text('Retry'.tr),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// مؤشّر التحميل الموحّد.
|
||||
class SiroLoading extends StatelessWidget {
|
||||
final String? label;
|
||||
const SiroLoading({super.key, this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 2.6,
|
||||
color: SiroUi.interactive(context),
|
||||
),
|
||||
if (label != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Text(label!, style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// صفّ «تسمية ← قيمة» داخل بطاقة تفاصيل.
|
||||
class SiroDetailRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData? icon;
|
||||
final Color? valueColor;
|
||||
|
||||
const SiroDetailRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.icon,
|
||||
this.valueColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
child: Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 17, color: SiroUi.subtle(context)),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(label, style: Theme.of(context).textTheme.bodySmall),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: valueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// رأس بارز بتدرّج العلامة — بديل الـ `SliverAppBar` الملوّن يدوياً في كل صفحة.
|
||||
class SiroHeroHeader extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final String? caption;
|
||||
final IconData? icon;
|
||||
final Widget? trailing;
|
||||
|
||||
const SiroHeroHeader({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.caption,
|
||||
this.icon,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: SiroUi.gap),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: SiroUi.brandGradient(context),
|
||||
borderRadius: BorderRadius.circular(SiroUi.cardRadius),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, color: Colors.white70, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (caption != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
caption!,
|
||||
style: const TextStyle(color: Colors.white60, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user