Files
Siro/siro_driver/lib/views/widgets/ui/siro_ui.dart
T

586 lines
17 KiB
Dart

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!,
],
),
);
}
}