chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق». نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا `cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules · .dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore. هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ. ⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
|
||||
class MyCircleContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Color backgroundColor;
|
||||
final Color borderColor;
|
||||
|
||||
MyCircleContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.backgroundColor = AppColor.secondaryColor,
|
||||
this.borderColor = AppColor.accentColor,
|
||||
});
|
||||
|
||||
final controller = Get.put(CircleController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<CircleController>(
|
||||
builder: ((controller) => GestureDetector(
|
||||
onTap: () {
|
||||
controller.changeColor();
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
onEnd: () {
|
||||
controller.onEnd();
|
||||
},
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: controller.size,
|
||||
height: controller.size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: controller.backgroundColor,
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(child: child),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
class CircleController extends GetxController {
|
||||
Color backgroundColor = AppColor.secondaryColor;
|
||||
double size = 40;
|
||||
void changeColor() {
|
||||
backgroundColor = backgroundColor == AppColor.secondaryColor
|
||||
? AppColor.accentColor
|
||||
: AppColor.secondaryColor;
|
||||
size = 60;
|
||||
update();
|
||||
}
|
||||
|
||||
void onEnd() {
|
||||
size = 40;
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
|
||||
class MyElevatedButton extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
final Color? kolor;
|
||||
final IconData? icon;
|
||||
|
||||
const MyElevatedButton({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
this.kolor,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (kolor ?? AppColor.accent).withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kolor ?? AppColor.accent,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onPressed();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: AppStyle.title.copyWith(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GlassContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double borderRadius;
|
||||
final List<Color>? gradientColors;
|
||||
final double blur;
|
||||
final double opacity;
|
||||
final bool showBorder;
|
||||
final Color? borderColor;
|
||||
|
||||
const GlassContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.width,
|
||||
this.height,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.borderRadius = 16.0,
|
||||
this.gradientColors,
|
||||
this.blur = 20.0,
|
||||
this.opacity = 0.1,
|
||||
this.showBorder = true,
|
||||
this.borderColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
final defaultGradient = isDark
|
||||
? [
|
||||
Colors.white.withValues(alpha: opacity),
|
||||
Colors.white.withValues(alpha: opacity * 0.5),
|
||||
]
|
||||
: [
|
||||
Colors.white.withValues(alpha: 0.7),
|
||||
Colors.white.withValues(alpha: 0.4),
|
||||
];
|
||||
|
||||
final effectiveBorderColor = borderColor ??
|
||||
(isDark
|
||||
? Colors.white.withValues(alpha: 0.15)
|
||||
: Colors.white.withValues(alpha: 0.5));
|
||||
|
||||
final shadowColor = isDark
|
||||
? Colors.black.withValues(alpha: 0.3)
|
||||
: cs.shadow.withValues(alpha: 0.08);
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: shadowColor,
|
||||
blurRadius: 24,
|
||||
spreadRadius: 0,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
||||
child: Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
border: showBorder
|
||||
? Border.all(color: effectiveBorderColor, width: 1)
|
||||
: null,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: gradientColors ?? defaultGradient,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
|
||||
class IconWidgetMenu extends StatelessWidget {
|
||||
const IconWidgetMenu({
|
||||
super.key,
|
||||
required this.onpressed,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final VoidCallback onpressed;
|
||||
final IconData icon;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onpressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 25),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.secondaryColor,
|
||||
offset: Offset(-2, -2),
|
||||
blurRadius: 0,
|
||||
spreadRadius: 0,
|
||||
blurStyle: BlurStyle.outer,
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColor.accentColor,
|
||||
offset: Offset(3, 3),
|
||||
blurRadius: 0,
|
||||
spreadRadius: 0,
|
||||
blurStyle: BlurStyle.outer,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 30,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: AppStyle.subtitle,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
|
||||
class MyScafolld extends StatelessWidget {
|
||||
const MyScafolld({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.action,
|
||||
required this.isleading,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<Widget> body;
|
||||
final Widget? action;
|
||||
final bool isleading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColor.bg,
|
||||
elevation: 0,
|
||||
leadingWidth: 70,
|
||||
leading: isleading
|
||||
? Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surfaceElevated,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: AppColor.textPrimary,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
if (action != null) action!,
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
title: Text(
|
||||
title,
|
||||
style: AppStyle.headTitle,
|
||||
),
|
||||
),
|
||||
body: SafeArea(child: Stack(children: body)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
|
||||
class MyTextForm extends StatelessWidget {
|
||||
const MyTextForm({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
required this.type,
|
||||
this.prefixIcon,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String label, hint;
|
||||
final TextInputType type;
|
||||
final IconData? prefixIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: TextFormField(
|
||||
keyboardType: type,
|
||||
style: AppStyle.body,
|
||||
cursorColor: AppColor.accent,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: AppColor.surface,
|
||||
prefixIcon: prefixIcon != null
|
||||
? Icon(prefixIcon, color: AppColor.textSecondary, size: 20)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: AppColor.divider),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: AppColor.accent, width: 2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: AppColor.danger),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: AppColor.danger, width: 2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
labelText: label.tr,
|
||||
labelStyle: AppStyle.subtitle,
|
||||
hintText: hint.tr,
|
||||
hintStyle: AppStyle.caption,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '${'Please enter'.tr} ${label.tr}';
|
||||
}
|
||||
|
||||
if (type == TextInputType.emailAddress) {
|
||||
if (!GetUtils.isEmail(value)) {
|
||||
return 'Please enter a valid email.'.tr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MyCircularProgressIndicator extends StatelessWidget {
|
||||
final Color backgroundColor;
|
||||
|
||||
const MyCircularProgressIndicator({
|
||||
super.key,
|
||||
this.backgroundColor = Colors.transparent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 110,
|
||||
height: 110,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset('assets/images/logo.png'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
|
||||
class DialogConfig {
|
||||
static const Duration animationDuration = Duration(milliseconds: 200);
|
||||
static const double blurStrength = 8.0;
|
||||
static const double cornerRadius = 14.0;
|
||||
static final BoxDecoration decoration = BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(cornerRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(38), // 0.15 opacity
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class MyDialog extends GetxController {
|
||||
void getDialog(String title, String? midTitle, VoidCallback onPressed) {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
Get.dialog(
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: DialogConfig.animationDuration,
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: 0.95 + (0.05 * value),
|
||||
child: Opacity(opacity: value, child: child),
|
||||
);
|
||||
},
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: DialogConfig.blurStrength,
|
||||
sigmaY: DialogConfig.blurStrength,
|
||||
),
|
||||
child: AlertDialog(
|
||||
backgroundColor: AppColor.surfaceElevated,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
title: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle,
|
||||
),
|
||||
content: midTitle != null
|
||||
? Text(
|
||||
midTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.subtitle,
|
||||
)
|
||||
: null,
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: AppColor.divider),
|
||||
),
|
||||
),
|
||||
child: Text('Cancel'.tr, style: AppStyle.subtitle),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
onPressed();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.accent,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text('OK'.tr, style: AppStyle.title.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierColor: Colors.black54,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyDialogContent extends GetxController {
|
||||
void getDialog(String title, Widget? content, VoidCallback onPressed) {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
Get.dialog(
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: DialogConfig.animationDuration,
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: 0.95 + (0.05 * value),
|
||||
child: Opacity(opacity: value, child: child),
|
||||
);
|
||||
},
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: DialogConfig.blurStrength,
|
||||
sigmaY: DialogConfig.blurStrength,
|
||||
),
|
||||
child: AlertDialog(
|
||||
backgroundColor: AppColor.surfaceElevated,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
title: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle,
|
||||
),
|
||||
content: content != null
|
||||
? SingleChildScrollView(child: content)
|
||||
: null,
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: AppColor.divider),
|
||||
),
|
||||
),
|
||||
child: Text('Cancel'.tr, style: AppStyle.subtitle),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
onPressed();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.accent,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text('OK'.tr, style: AppStyle.title.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierColor: Colors.black54,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ResponsiveLayout extends StatelessWidget {
|
||||
final Widget mobile;
|
||||
final Widget desktop;
|
||||
|
||||
const ResponsiveLayout({
|
||||
Key? key,
|
||||
required this.mobile,
|
||||
required this.desktop,
|
||||
}) : super(key: key);
|
||||
|
||||
static bool isDesktop(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width >= 900; // standard desktop breakpoint
|
||||
}
|
||||
|
||||
static bool isMobile(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width < 900;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth >= 900) {
|
||||
return desktop;
|
||||
} else {
|
||||
return mobile;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
enum _SnackVariant { success, error, info, warning }
|
||||
|
||||
extension _VariantProps on _SnackVariant {
|
||||
Color get baseColor => switch (this) {
|
||||
_SnackVariant.success => const Color(0xFF1A9E5C),
|
||||
_SnackVariant.error => const Color(0xFFD93025),
|
||||
_SnackVariant.info => const Color(0xFF1A73E8),
|
||||
_SnackVariant.warning => const Color(0xFFF29900),
|
||||
};
|
||||
|
||||
Color get surfaceColor => switch (this) {
|
||||
_SnackVariant.success => const Color(0xFFF0FBF5),
|
||||
_SnackVariant.error => const Color(0xFFFEF2F1),
|
||||
_SnackVariant.info => const Color(0xFFF0F6FF),
|
||||
_SnackVariant.warning => const Color(0xFFFFF8E6),
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
_SnackVariant.success => Icons.check_circle_rounded,
|
||||
_SnackVariant.error => Icons.error_rounded,
|
||||
_SnackVariant.info => Icons.info_rounded,
|
||||
_SnackVariant.warning => Icons.warning_amber_rounded,
|
||||
};
|
||||
|
||||
String get label => switch (this) {
|
||||
_SnackVariant.success => 'Success',
|
||||
_SnackVariant.error => 'Error',
|
||||
_SnackVariant.info => 'Info',
|
||||
_SnackVariant.warning => 'Warning',
|
||||
};
|
||||
}
|
||||
|
||||
class _SnackContent extends StatefulWidget {
|
||||
final String message;
|
||||
final _SnackVariant variant;
|
||||
|
||||
const _SnackContent({required this.message, required this.variant});
|
||||
|
||||
@override
|
||||
State<_SnackContent> createState() => _SnackContentState();
|
||||
}
|
||||
|
||||
class _SnackContentState extends State<_SnackContent>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _ctrl;
|
||||
late final AnimationController _scaleCtrl;
|
||||
late final Animation<double> _scaleAnim;
|
||||
late final Animation<double> _progressAnim;
|
||||
|
||||
static const Duration _displayDuration = Duration(seconds: 4);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(vsync: this, duration: _displayDuration);
|
||||
_scaleCtrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
_scaleAnim = CurvedAnimation(
|
||||
parent: _scaleCtrl,
|
||||
curve: Curves.elasticOut,
|
||||
);
|
||||
_progressAnim = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _ctrl, curve: Curves.linear),
|
||||
);
|
||||
_scaleCtrl.forward();
|
||||
_ctrl.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
_scaleCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final v = widget.variant;
|
||||
final accent = v.baseColor;
|
||||
final surface = v.surfaceColor;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: surface,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: accent.withAlpha(46), width: 1.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accent.withAlpha(31),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -2,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(15),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 10, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: _scaleAnim,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withAlpha(31),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(v.icon, color: accent, size: 22),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
v.label.tr,
|
||||
style: TextStyle(
|
||||
color: accent,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
widget.message,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF424242),
|
||||
fontSize: 13.5,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
_closeSnackbar(context);
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withAlpha(25),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: 16,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: _progressAnim,
|
||||
builder: (_, __) => Stack(
|
||||
children: [
|
||||
Container(height: 3, color: accent.withAlpha(20)),
|
||||
FractionallySizedBox(
|
||||
widthFactor: _progressAnim.value,
|
||||
child: Container(
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withAlpha(115),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(4),
|
||||
bottomRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _closeSnackbar(BuildContext context) {
|
||||
ScaffoldMessenger.maybeOf(context)?.hideCurrentSnackBar();
|
||||
}
|
||||
}
|
||||
|
||||
int _retryCount = 0;
|
||||
|
||||
void _show(_SnackVariant variant, String message) {
|
||||
if (Get.context == null) {
|
||||
if (_retryCount < 3) {
|
||||
_retryCount++;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _show(variant, message));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_retryCount = 0;
|
||||
|
||||
final context = Get.context;
|
||||
if (context == null) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
if (messenger == null) return;
|
||||
|
||||
messenger.clearSnackBars();
|
||||
|
||||
switch (variant) {
|
||||
case _SnackVariant.error:
|
||||
case _SnackVariant.warning:
|
||||
HapticFeedback.mediumImpact();
|
||||
case _SnackVariant.success:
|
||||
HapticFeedback.lightImpact();
|
||||
case _SnackVariant.info:
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: _SnackContent(message: message, variant: variant),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 4),
|
||||
dismissDirection: DismissDirection.up,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void mySnackbarSuccess(String message) => _show(_SnackVariant.success, message);
|
||||
void mySnackbarError(String message) => _show(_SnackVariant.error, message);
|
||||
void mySnackbarInfo(String message) => _show(_SnackVariant.info, message);
|
||||
void mySnackbarWarning(String message) => _show(_SnackVariant.warning, message);
|
||||
@@ -0,0 +1,483 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class UnifiedAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final Widget? leading;
|
||||
final List<Widget>? actions;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final double elevation;
|
||||
final bool centerTitle;
|
||||
final PreferredSizeWidget? bottom;
|
||||
final double toolbarHeight;
|
||||
|
||||
const UnifiedAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.elevation = 0,
|
||||
this.centerTitle = false,
|
||||
this.bottom,
|
||||
this.toolbarHeight = kToolbarHeight + 34,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final bgColor = backgroundColor ?? cs.surface;
|
||||
|
||||
return Container(
|
||||
height: toolbarHeight,
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
border: Border(bottom: BorderSide(color: cs.outline, width: 0.5)),
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Row(
|
||||
children: [
|
||||
leading ??
|
||||
GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: foregroundColor ?? cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (actions != null) ...actions!,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(toolbarHeight);
|
||||
}
|
||||
|
||||
class UnifiedAppBarWithTabs extends StatelessWidget implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final List<Widget> tabs;
|
||||
final TabController? tabController;
|
||||
final Widget? leading;
|
||||
final List<Widget>? actions;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final double elevation;
|
||||
final EdgeInsetsGeometry? tabPadding;
|
||||
|
||||
const UnifiedAppBarWithTabs({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.tabs,
|
||||
this.tabController,
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.elevation = 0,
|
||||
this.tabPadding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final bgColor = backgroundColor ?? cs.surface;
|
||||
|
||||
return Container(
|
||||
height: kToolbarHeight + 34 + 48,
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
border: Border(bottom: BorderSide(color: cs.outline, width: 0.5)),
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
leading ??
|
||||
GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: foregroundColor ?? cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (actions != null) ...actions!,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: tabPadding ?? const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
isScrollable: true,
|
||||
indicator: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
dividerColor: Colors.transparent,
|
||||
labelColor: cs.primary,
|
||||
unselectedLabelColor: cs.onSurfaceVariant,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 13),
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 34 + 48);
|
||||
}
|
||||
|
||||
class UnifiedScaffold extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget body;
|
||||
final List<Widget>? actions;
|
||||
final Widget? leading;
|
||||
final Color? backgroundColor;
|
||||
final bool extendBody;
|
||||
final FloatingActionButton? floatingActionButton;
|
||||
final FloatingActionButtonLocation? floatingActionButtonLocation;
|
||||
final PreferredSizeWidget? bottomNavigationBar;
|
||||
|
||||
const UnifiedScaffold({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.actions,
|
||||
this.leading,
|
||||
this.backgroundColor,
|
||||
this.extendBody = false,
|
||||
this.floatingActionButton,
|
||||
this.floatingActionButtonLocation,
|
||||
this.bottomNavigationBar,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: backgroundColor ?? cs.surface,
|
||||
extendBody: extendBody,
|
||||
floatingActionButton: floatingActionButton,
|
||||
floatingActionButtonLocation: floatingActionButtonLocation,
|
||||
bottomNavigationBar: bottomNavigationBar,
|
||||
appBar: UnifiedAppBar(
|
||||
title: title,
|
||||
actions: actions,
|
||||
leading: leading,
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedLoadingState extends StatelessWidget {
|
||||
final String? message;
|
||||
final Color? color;
|
||||
|
||||
const UnifiedLoadingState({super.key, this.message, this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: color ?? cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: (color ?? cs.primary).withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message ?? 'جاري التحميل...',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedEmptyState extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget? action;
|
||||
|
||||
const UnifiedEmptyState({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.action,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (action != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
action!,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedStatCard extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback? onTap;
|
||||
final String? subtitle;
|
||||
|
||||
const UnifiedStatCard({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
splashColor: color.withValues(alpha: 0.1),
|
||||
highlightColor: color.withValues(alpha: 0.05),
|
||||
child: Container(
|
||||
width: 155,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 16),
|
||||
),
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedSectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData? icon;
|
||||
final Color? iconColor;
|
||||
final Widget? trailing;
|
||||
|
||||
const UnifiedSectionHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.icon,
|
||||
this.iconColor,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor ?? cs.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (icon != null) ...[
|
||||
Icon(icon!, size: 16, color: iconColor ?? cs.primary),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const Spacer(),
|
||||
trailing!,
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/main.dart';
|
||||
import 'package:siro_admin/constant/box_name.dart';
|
||||
import 'package:siro_admin/views/admin/captain/captain.dart';
|
||||
import 'package:siro_admin/views/admin/passenger/passenger.dart';
|
||||
import 'package:siro_admin/views/admin/drivers/driver_tracker_screen.dart';
|
||||
import 'package:siro_admin/views/transit/org_list_page.dart';
|
||||
import 'package:siro_admin/views/admin/static/static.dart';
|
||||
import 'package:siro_admin/controller/admin/static_controller.dart';
|
||||
import 'package:siro_admin/views/admin/drivers/monitor_ride.dart';
|
||||
import 'package:siro_admin/views/admin/rides/ride_lookup_page.dart';
|
||||
import 'package:siro_admin/views/admin/packages.dart';
|
||||
import 'package:siro_admin/views/admin/wallet/wallet.dart';
|
||||
import 'package:siro_admin/views/admin/captain/syrian_driver_not_active.dart';
|
||||
import 'package:siro_admin/views/admin/drivers/driver_gift_check_page.dart';
|
||||
import 'package:siro_admin/views/admin/quality/blacklist_page.dart';
|
||||
import 'package:siro_admin/views/admin/employee/employee_page.dart';
|
||||
import 'package:siro_admin/views/admin/server/monitor_server_page.dart';
|
||||
import 'package:siro_admin/views/admin/error/error/error_page.dart';
|
||||
import 'package:siro_admin/views/invoice/invoice_list_page.dart';
|
||||
import 'package:siro_admin/views/admin/staff/add_staff_page.dart';
|
||||
import 'package:siro_admin/views/admin/staff/pending_admins_page.dart';
|
||||
import 'package:siro_admin/views/admin/static/advanced_analytics_page.dart';
|
||||
import 'package:siro_admin/views/admin/financial/financial_v2_page.dart';
|
||||
import 'package:siro_admin/views/admin/security/audit_logs_page.dart';
|
||||
import 'package:siro_admin/views/admin/analytics/live_analytics_page.dart';
|
||||
|
||||
class WebSidebar extends StatelessWidget {
|
||||
final int selectedIndex;
|
||||
final Function(int)? onItemSelected;
|
||||
|
||||
const WebSidebar({
|
||||
Key? key,
|
||||
this.selectedIndex = 0,
|
||||
this.onItemSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final String myPhone = box.read(BoxName.adminPhone)?.toString() ?? '';
|
||||
final String role = box.read('admin_role')?.toString() ?? 'admin';
|
||||
final bool isSuperAdmin = (role == 'super_admin') ||
|
||||
(myPhone == '201023248456' ||
|
||||
myPhone == '963992952235' ||
|
||||
myPhone == '962798583052');
|
||||
|
||||
return Container(
|
||||
width: 270,
|
||||
color: cs.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildLogo(context),
|
||||
Divider(color: cs.outline, height: 1),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
children: [
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'اللوحة الرئيسية',
|
||||
icon: Icons.dashboard_rounded,
|
||||
index: 0,
|
||||
onTap: () {
|
||||
if (onItemSelected != null) onItemSelected!(0);
|
||||
},
|
||||
),
|
||||
|
||||
_buildSectionHeader(context, 'التحليلات والإحصائيات'),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'التحليلات الحية',
|
||||
icon: Icons.ssid_chart_rounded,
|
||||
index: 101,
|
||||
onTap: () => Get.to(() => const LiveAnalyticsPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'التحليلات المتقدمة',
|
||||
icon: Icons.analytics_rounded,
|
||||
index: 102,
|
||||
onTap: () => Get.to(() => const AdvancedAnalyticsPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'إحصائيات النظام',
|
||||
icon: Icons.bar_chart_rounded,
|
||||
index: 7,
|
||||
onTap: () async {
|
||||
await Get.put(StaticController()).getAll();
|
||||
Get.to(() => const StaticDash());
|
||||
},
|
||||
),
|
||||
|
||||
_buildSectionHeader(context, 'الكباتن والركاب'),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'إدارة الركاب',
|
||||
icon: Icons.people_outline_rounded,
|
||||
index: 1,
|
||||
onTap: () => Get.to(() => Passengrs()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'قائمة الكباتن',
|
||||
icon: Icons.drive_eta_rounded,
|
||||
index: 2,
|
||||
onTap: () => Get.to(() => CaptainsPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'طلبات الكباتن المعلقة',
|
||||
icon: Icons.person_add_disabled_rounded,
|
||||
index: 103,
|
||||
onTap: () => Get.to(() => DriversPendingPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'مراجعة الوثائق',
|
||||
icon: Icons.assignment_ind_rounded,
|
||||
index: 104,
|
||||
onTap: () => Get.toNamed('/driver-docs'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'هدايا وعروض الكباتن',
|
||||
icon: Icons.card_giftcard_rounded,
|
||||
index: 105,
|
||||
onTap: () => Get.to(() => const DriverGiftCheckPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'القائمة السوداء',
|
||||
icon: Icons.block_rounded,
|
||||
index: 106,
|
||||
onTap: () => Get.to(() => const BlacklistPage()),
|
||||
),
|
||||
|
||||
_buildSectionHeader(context, 'الرحلات والتتبع'),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'شاشة الرحلات',
|
||||
icon: Icons.map_rounded,
|
||||
index: 5,
|
||||
onTap: () => Get.to(() => RidesDashboardScreen()),
|
||||
),
|
||||
if (isSuperAdmin)
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'مراقبة الرحلات الحية',
|
||||
icon: Icons.remove_red_eye_rounded,
|
||||
index: 6,
|
||||
onTap: () => Get.to(() => RideMonitorScreen()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'تتبع الكباتن (المراقب)',
|
||||
icon: Icons.track_changes_rounded,
|
||||
index: 3,
|
||||
onTap: () => Get.to(() => SiroTrackerScreen()),
|
||||
),
|
||||
|
||||
_buildSectionHeader(context, 'نظام مواصلاتي'),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'إدارة المؤسسات والشركات',
|
||||
icon: Icons.directions_bus_filled_rounded,
|
||||
index: 4,
|
||||
onTap: () => Get.to(() => const TransitOrgListPage()),
|
||||
),
|
||||
|
||||
_buildSectionHeader(context, 'المالية والأسعار'),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'التقرير المالي (V2)',
|
||||
icon: Icons.account_balance_wallet_rounded,
|
||||
index: 108,
|
||||
onTap: () => Get.to(() => const FinancialV2Page()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'المحافظ المالية',
|
||||
icon: Icons.account_balance_rounded,
|
||||
index: 109,
|
||||
onTap: () => Get.to(() => Wallet()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'الباقات والاشتراكات',
|
||||
icon: Icons.card_membership_rounded,
|
||||
index: 110,
|
||||
onTap: () => Get.to(() => PackageUpdateScreen()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'الفواتير',
|
||||
icon: Icons.receipt_long_rounded,
|
||||
index: 111,
|
||||
onTap: () => Get.to(() => const InvoiceListPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'تسعير الكازان',
|
||||
icon: Icons.price_change_rounded,
|
||||
index: 9,
|
||||
onTap: () => Get.toNamed('/kazan'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'أكواد الخصم (Promo)',
|
||||
icon: Icons.confirmation_number_rounded,
|
||||
index: 8,
|
||||
onTap: () => Get.toNamed('/promo'),
|
||||
),
|
||||
|
||||
_buildSectionHeader(context, 'التسويق والدعم'),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'حركات التسويق والـ AI',
|
||||
icon: Icons.campaign_rounded,
|
||||
index: 112,
|
||||
onTap: () => Get.toNamed('/marketing'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'الخريطة الحرارية (Heatmap)',
|
||||
icon: Icons.local_fire_department_rounded,
|
||||
index: 113,
|
||||
onTap: () => Get.toNamed('/heatmap'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'الذكاء الاجتماعي',
|
||||
icon: Icons.psychology_rounded,
|
||||
index: 114,
|
||||
onTap: () => Get.toNamed('/social-intelligence'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'إدارة الشكاوى',
|
||||
icon: Icons.report_problem_rounded,
|
||||
index: 115,
|
||||
onTap: () => Get.toNamed('/complaints'),
|
||||
),
|
||||
|
||||
if (isSuperAdmin) ...[
|
||||
_buildSectionHeader(context, 'إدارة النظام والأمان'),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'مراقبة السيرفرات',
|
||||
icon: Icons.dns_rounded,
|
||||
index: 116,
|
||||
onTap: () => Get.to(() => ServerMonitorPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'سجلات الأمان والعمليات',
|
||||
icon: Icons.admin_panel_settings_outlined,
|
||||
index: 117,
|
||||
onTap: () => Get.to(() => const AuditLogsPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'فريق العمل والموظفين',
|
||||
icon: Icons.badge_rounded,
|
||||
index: 118,
|
||||
onTap: () => Get.to(() => EmployeePage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'إضافة مشرف جديد',
|
||||
icon: Icons.person_add_rounded,
|
||||
index: 119,
|
||||
onTap: () => Get.to(() => const AddStaffPage(role: 'admin')),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'طلبات المشرفين المعلقة',
|
||||
icon: Icons.hourglass_top_rounded,
|
||||
index: 120,
|
||||
onTap: () => Get.to(() => const PendingAdminsPage()),
|
||||
),
|
||||
_buildNavItem(
|
||||
context,
|
||||
title: 'أخطاء النظام',
|
||||
icon: Icons.bug_report_rounded,
|
||||
index: 121,
|
||||
onTap: () => Get.to(() => const ErrorListPage()),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(color: cs.outline, height: 1),
|
||||
_buildThemeToggle(context),
|
||||
_buildLogoutBtn(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogo(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Icon(Icons.admin_panel_settings_rounded,
|
||||
color: cs.primary, size: 28),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'SIRO ADMIN',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Web Dashboard',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 20, left: 20, top: 18, bottom: 6),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: cs.primary.withValues(alpha: 0.85),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required int index,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final bool isSelected = selectedIndex == index;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
hoverColor: cs.primary.withValues(alpha: 0.08),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? cs.primary.withValues(alpha: 0.15)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: isSelected
|
||||
? Border.all(color: cs.primary.withValues(alpha: 0.4))
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: isSelected ? cs.primary : cs.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSelected ? cs.primary : cs.onSurface,
|
||||
fontSize: 13.5,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeToggle(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final newMode = isDark ? ThemeMode.light : ThemeMode.dark;
|
||||
Get.changeThemeMode(newMode);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
hoverColor: cs.primary.withValues(alpha: 0.08),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Icon(
|
||||
isDark ? Icons.light_mode_rounded : Icons.dark_mode_rounded,
|
||||
key: ValueKey(isDark),
|
||||
size: 20,
|
||||
color: isDark ? const Color(0xFFFBBF24) : cs.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isDark ? 'الوضع الفاتح' : 'الوضع الداكن',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 44,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? cs.primary.withValues(alpha: 0.3)
|
||||
: cs.outline,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
alignment: isDark ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? cs.primary : cs.onSurface,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogoutBtn(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
box.erase();
|
||||
Get.offAllNamed('/login');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.error.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.error.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.logout_rounded, color: cs.error, size: 20),
|
||||
const SizedBox(width: 14),
|
||||
Text(
|
||||
'تسجيل الخروج',
|
||||
style: TextStyle(
|
||||
color: cs.error,
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user