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