From 08132b52a9db36213959748d7095b4fc1f929711 Mon Sep 17 00:00:00 2001 From: Daniah Ayad Al-sultani <148902945+Cactuskiller@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:23:06 +0300 Subject: [PATCH] login agnimation has been fixed + request holiday screen was created --- assets/images/back.svg | 3 + assets/images/calendar.svg | 3 + assets/images/money.svg | 2 +- assets/images/money2.svg | 3 + lib/screens/attendence_screen.dart | 6 +- lib/screens/holiday_screen.dart | 121 +++++- lib/screens/request_leave_screen.dart | 547 ++++++++++++++++++++++++ lib/widgets/login_animation.dart | 486 +++++++++++++++++++++ lib/widgets/login_animation_screen.dart | 353 --------------- lib/widgets/settings_bar.dart | 103 ++--- pubspec.lock | 16 +- 11 files changed, 1200 insertions(+), 443 deletions(-) create mode 100644 assets/images/back.svg create mode 100644 assets/images/calendar.svg create mode 100644 assets/images/money2.svg create mode 100644 lib/screens/request_leave_screen.dart create mode 100644 lib/widgets/login_animation.dart delete mode 100644 lib/widgets/login_animation_screen.dart diff --git a/assets/images/back.svg b/assets/images/back.svg new file mode 100644 index 0000000..1816639 --- /dev/null +++ b/assets/images/back.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/calendar.svg b/assets/images/calendar.svg new file mode 100644 index 0000000..5b73799 --- /dev/null +++ b/assets/images/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/money.svg b/assets/images/money.svg index 5a498c5..9db1b4b 100644 --- a/assets/images/money.svg +++ b/assets/images/money.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/images/money2.svg b/assets/images/money2.svg new file mode 100644 index 0000000..345bedb --- /dev/null +++ b/assets/images/money2.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/screens/attendence_screen.dart b/lib/screens/attendence_screen.dart index ddea7b8..b9478fa 100644 --- a/lib/screens/attendence_screen.dart +++ b/lib/screens/attendence_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../widgets/login_animation_screen.dart'; // Import the LoginAnimationScreen +import '../widgets/login_animation.dart'; class AttendanceScreen extends StatelessWidget { const AttendanceScreen({super.key}); @@ -153,8 +153,8 @@ class AttendanceScreen extends StatelessWidget { MaterialPageRoute( builder: (context) => LoginAnimationScreen( - isLogin: true, - isSuccess: false, + isLogin: false, + isSuccess: true, ), ), ); diff --git a/lib/screens/holiday_screen.dart b/lib/screens/holiday_screen.dart index de3643d..9d1f92c 100644 --- a/lib/screens/holiday_screen.dart +++ b/lib/screens/holiday_screen.dart @@ -1,9 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../screens/request_leave_screen.dart'; -class HolidayScreen extends StatelessWidget { +class HolidayScreen extends StatefulWidget { const HolidayScreen({super.key}); + @override + State createState() => _HolidayScreenState(); +} + +class _HolidayScreenState extends State { + int activeTab = 0; // 0 = السلف | 1 = الأجازات + @override Widget build(BuildContext context) { return Directionality( @@ -11,20 +19,99 @@ class HolidayScreen extends StatelessWidget { child: Stack( children: [ Positioned( - bottom: 40, + top: 40, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // الأجازات TAB + GestureDetector( + onTap: () => setState(() => activeTab = 1), + child: Column( + children: [ + Text( + "الأجازات", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: + activeTab == 1 + ? const Color(0xFF8EFDC2) + : const Color(0x9EFFFFFF), + ), + ), + if (activeTab == 1) + Container( + width: 60, + height: 2, + margin: const EdgeInsets.only(top: 4), + color: const Color(0xFF8EFDC2), + ), + ], + ), + ), + + const SizedBox(width: 70), + + // السلف TAB + GestureDetector( + onTap: () => setState(() => activeTab = 0), + child: Column( + children: [ + Text( + "السلف", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: + activeTab == 0 + ? const Color(0xFF8EFDC2) + : const Color(0x9EFFFFFF), + ), + ), + if (activeTab == 0) + Container( + width: 60, + height: 2, + margin: const EdgeInsets.only(top: 4), + color: const Color(0xFF8EFDC2), + ), + ], + ), + ), + ], + ), + ), + Positioned( + bottom: 30, right: 20, child: Column( children: [ + // Money icon (custom size) _HolidayActionButton( label: "طلب سلفة", - svgPath: "assets/images/money.svg", // placeholder + svgPath: "assets/images/money2.svg", + iconWidth: 38, + iconHeight: 30, onTap: () {}, ), const SizedBox(height: 18), + + // Plus icon (custom size) _HolidayActionButton( label: "طلب إجازة", - svgPath: "assets/images/plus.svg", // placeholder - onTap: () {}, + svgPath: "assets/images/plus.svg", + iconWidth: 30, + iconHeight: 30, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RequestLeaveScreen(), + ), + ); + }, ), ], ), @@ -39,11 +126,15 @@ class _HolidayActionButton extends StatelessWidget { final String label; final String svgPath; final VoidCallback onTap; + final double iconWidth; + final double iconHeight; const _HolidayActionButton({ required this.label, required this.svgPath, required this.onTap, + required this.iconWidth, + required this.iconHeight, }); @override @@ -51,28 +142,26 @@ class _HolidayActionButton extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - width: 75, - height: 75, + width: 80, + height: 80, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, - boxShadow: [ + boxShadow: const [ BoxShadow( - color: const Color(0x4B00C68B), + color: Color(0x4B00C68B), blurRadius: 20, - offset: const Offset(0, 6), + offset: Offset(0, 6), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - SvgPicture.asset( - svgPath, - width: 32, - height: 32, - fit: BoxFit.contain, // 🔥 Ensures same icon size - alignment: Alignment.center, + SizedBox( + width: iconWidth, + height: iconHeight, + child: SvgPicture.asset(svgPath, fit: BoxFit.contain), ), const SizedBox(height: 6), diff --git a/lib/screens/request_leave_screen.dart b/lib/screens/request_leave_screen.dart new file mode 100644 index 0000000..074a5c0 --- /dev/null +++ b/lib/screens/request_leave_screen.dart @@ -0,0 +1,547 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import '../widgets/app_background.dart'; +import '../widgets/settings_bar.dart'; +import '../widgets/onboarding_button.dart'; + +class RequestLeaveScreen extends StatefulWidget { + const RequestLeaveScreen({super.key}); + + @override + State createState() => _RequestLeaveScreenState(); +} + +class _RequestLeaveScreenState extends State { + // Dropdown value + String leaveType = "إجازة مرضية "; + + // Toggle switch + bool isTimedLeave = false; + + // Date & time selectors + DateTime? fromDate = DateTime.now(); + DateTime? toDate = DateTime.now(); + + TimeOfDay? fromTime = const TimeOfDay(hour: 12, minute: 00); + TimeOfDay? toTime = const TimeOfDay(hour: 12, minute: 00); + + // Text controller for reason + final TextEditingController reasonController = TextEditingController(); + + /// PICK DATE + Future pickDate(bool isFrom) async { + DateTime initial = isFrom ? fromDate! : toDate!; + DateTime? newDate = await showDatePicker( + context: context, + initialDate: initial, + firstDate: DateTime(2020), + lastDate: DateTime(2035), + builder: (context, child) { + return Theme(data: ThemeData.dark(), child: child!); + }, + ); + + if (newDate != null) { + setState(() { + if (isFrom) { + fromDate = newDate; + } else { + toDate = newDate; + } + }); + } + } + + /// PICK TIME + Future pickTime(bool isFrom) async { + TimeOfDay initial = isFrom ? fromTime! : toTime!; + TimeOfDay? newTime = await showTimePicker( + context: context, + initialTime: initial, + builder: (context, child) { + return Theme(data: ThemeData.dark(), child: child!); + }, + ); + + if (newTime != null) { + setState(() { + if (isFrom) { + fromTime = newTime; + } else { + toTime = newTime; + } + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AppBackground( + child: SafeArea( + child: Column( + children: [ + // ============================= + // TOP NAV BAR + // ============================= + SettingsBar( + selectedIndex: -1, + onTap: (_) {}, + showBackButton: true, + onBackTap: () => Navigator.pop(context), + iconPaths: const [ + "assets/images/user.svg", + "assets/images/bell.svg", + ], + ), + + // Title + const SizedBox(height: 30), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.topRight, + child: const Text( + "طلب أجازة ", + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + ), + + const SizedBox(height: 25), + //============================= + // DROPDOWN: نوع الإجازة + //============================= + Align( + alignment: Alignment.centerRight, + child: const Text( + "نوع الإجازة", + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + + const SizedBox(height: 6), + + Directionality( + textDirection: + TextDirection.rtl, // <<< CHANGE DIRECTION HERE + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20), + height: 58, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: const [ + BoxShadow( + color: Color(0x22000000), + blurRadius: 8, + offset: Offset(0, 3), + ), + ], + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: leaveType, + icon: const Icon( + Icons.keyboard_arrow_down_rounded, + color: Colors.black, + size: 28, + ), + style: const TextStyle( + color: Colors.black, + fontSize: 17, + ), + isExpanded: true, + onChanged: (value) { + setState(() => leaveType = value!); + }, + items: [ + DropdownMenuItem( + value: "إجازة مرضية ", + child: Directionality( + textDirection: TextDirection.rtl, + child: Align( + alignment: Alignment.centerRight, + child: Text("إجازة مرضية "), + ), + ), + ), + DropdownMenuItem( + value: "إجازة مدفوعة", + child: Directionality( + textDirection: TextDirection.rtl, + child: Align( + alignment: Alignment.centerRight, + child: Text("إجازة مدفوعة"), + ), + ), + ), + DropdownMenuItem( + value: "إجازة غير مدفوعة", + child: Directionality( + textDirection: TextDirection.rtl, + child: Align( + alignment: Alignment.centerRight, + child: Text("إجازة غير مدفوعة"), + ), + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 25), + + //============================= + // PERFECT CUSTOM TOGGLE (NEW) + //============================= + GestureDetector( + onTap: + () => setState(() => isTimedLeave = !isTimedLeave), + child: Row( + children: [ + // ---------- TOGGLE ---------- + AnimatedContainer( + duration: const Duration(milliseconds: 250), + width: 95, + height: 42, + padding: const EdgeInsets.symmetric( + horizontal: 4, + ), + decoration: BoxDecoration( + color: + isTimedLeave + ? const Color( + 0xFF0A6B4A, + ) // ON green track + : const Color( + 0xFF9E9E9E, + ), // OFF grey track + borderRadius: BorderRadius.circular(40), + ), + child: Align( + alignment: + isTimedLeave + ? Alignment.centerRight + : Alignment.centerLeft, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + width: 40, + height: 40, + decoration: BoxDecoration( + color: + isTimedLeave + ? const Color(0xFF12BE85) // ON knob + : Colors.white, // OFF knob + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color(0x2D000000), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + ), + + const SizedBox(width: 14), + + // ---------- Dot (ALWAYS visible) ---------- + Container( + width: 10, + height: 10, + decoration: const BoxDecoration( + color: Color(0xFF32C59A), + shape: BoxShape.circle, + ), + ), + + const SizedBox(width: 6), + + // ---------- Line (ALWAYS visible) ---------- + Expanded( + child: Container( + height: 2, + color: const Color(0xFF32C59A), + ), + ), + + const SizedBox(width: 12), + + // ---------- Label ---------- + const Text( + "إجازة زمنية", + style: TextStyle( + color: Colors.white, + fontSize: 19, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // ============================= + // DATE PICKER BOX + // ============================= + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF4F4F4), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x33000000), + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.end, // ALIGN RIGHT + children: [ + // TEXT aligned right + const Text( + "فترة الإجازة", + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox(width: 8), + + // 🟢 YOUR CUSTOM SVG ICON + SvgPicture.asset( + "assets/images/calendar.svg", // <-- replace with your icon filename + width: 24, + height: 24, + color: Color( + 0xFF007C46, + ), // Optional: match your green + ), + ], + ), + + const SizedBox(height: 15), + + // From date row + _dateRow( + label: "من", + date: fromDate!, + time: fromTime!, + onDateTap: + isTimedLeave ? null : () => pickDate(true), + onTimeTap: () => pickTime(true), + ), + + const SizedBox(height: 15), + + // To date row + _dateRow( + label: "الى", + date: toDate!, + time: toTime!, + onDateTap: + isTimedLeave ? null : () => pickDate(false), + onTimeTap: () => pickTime(false), + ), + ], + ), + ), + + const SizedBox(height: 25), + + // ============================= + // REASON TEXTFIELD (Two Containers) + // ============================= + Align( + alignment: Alignment.centerRight, + child: Directionality( + textDirection: TextDirection.rtl, + child: const Text( + "السبب", + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + // OUTER BORDER CONTAINER + Container( + padding: const EdgeInsets.all( + 15, + ), // border thickness space + decoration: BoxDecoration( + border: Border.all( + color: Color(0xFF00FFAA), // green border + width: 0.5, + ), + borderRadius: BorderRadius.circular(14), + ), + + // INNER TEXTFIELD CONTAINER + child: Container( + height: 100, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + decoration: BoxDecoration( + color: const Color(0xFFEAEAEA), + borderRadius: BorderRadius.circular(12), + ), + child: TextField( + controller: reasonController, + maxLines: 5, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: "", + ), + ), + ), + ), + + const SizedBox(height: 40), + + // CONFIRM BUTTON + Center( + child: OnboardingButton( + text: "تأكيد الطلب", + backgroundColor: const Color(0xFFD1FEF0), + onPressed: () {}, + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + // =============================================================== + // CUSTOM DATE ROW WIDGET + // =============================================================== + Widget _dateRow({ + required String label, + required DateTime date, + required TimeOfDay time, + required VoidCallback? onDateTap, + required VoidCallback onTimeTap, + }) { + bool dateDisabled = onDateTap == null; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + height: 55, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + // ----------------------- + // DATE PART (can be disabled) + // ----------------------- + Opacity( + opacity: dateDisabled ? 0.45 : 1, + child: IgnorePointer( + ignoring: dateDisabled, + child: GestureDetector( + onTap: onDateTap, + child: Row( + children: [ + const Icon(Icons.arrow_drop_down), + const SizedBox(width: 4), + Text( + "${date.year}-${date.month}-${date.day}", + style: const TextStyle(fontSize: 16), + ), + const SizedBox(width: 10), + Text( + _weekday(date.weekday), + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ), + + const Spacer(), + + // ----------------------- + // TIME PART (always active) + // ----------------------- + GestureDetector( + onTap: onTimeTap, + child: Text( + "${time.hour}:${time.minute.toString().padLeft(2, '0')}", + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + + const SizedBox(width: 10), + + Text( + label, + style: const TextStyle(color: Colors.black, fontSize: 15), + ), + ], + ), + ); + } + + String _weekday(int day) { + switch (day) { + case 1: + return "الإثنين"; + case 2: + return "الثلاثاء"; + case 3: + return "الأربعاء"; + case 4: + return "الخميس"; + case 5: + return "الجمعة"; + case 6: + return "السبت"; + case 7: + return "الأحد"; + default: + return ""; + } + } +} diff --git a/lib/widgets/login_animation.dart b/lib/widgets/login_animation.dart new file mode 100644 index 0000000..126749c --- /dev/null +++ b/lib/widgets/login_animation.dart @@ -0,0 +1,486 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import '../widgets/app_background.dart'; + +class LoginAnimationScreen extends StatefulWidget { + final bool isLogin; + final bool isSuccess; + + const LoginAnimationScreen({ + super.key, + required this.isLogin, + required this.isSuccess, + }); + + @override + State createState() => _LoginAnimationScreenState(); +} + +class _LoginAnimationScreenState extends State + with TickerProviderStateMixin { + late AnimationController ringController; + late AnimationController progressController; + late Animation ringAnimation; + late Animation progressAnimation; + + // ERROR pulse animation + late AnimationController errorPulseController; + late Animation errorPulseAnimation; + + // SUCCESS one-time pulse + late AnimationController successPulseController; + late Animation successPulseAnimation; + + bool showResult = false; + + @override + void initState() { + super.initState(); + + // MAIN loading ripple animations + ringController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + progressController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + ); + + ringAnimation = Tween( + begin: 0, + end: 1, + ).animate(CurvedAnimation(parent: ringController, curve: Curves.easeOut)); + + progressAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: progressController, curve: Curves.linear), + ); + + // ERROR pulse animation + errorPulseController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + errorPulseAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: errorPulseController, curve: Curves.easeOut), + ); + + // SUCCESS one-time pulse + successPulseController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + successPulseAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: successPulseController, curve: Curves.easeOut), + ); + + startAnimations(); + + // MAIN DELAY (same as before) + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() => showResult = true); + + ringController.stop(); + progressController.stop(); + + if (!widget.isSuccess) { + startErrorPulse(); + } else { + successPulseController.forward(); // ONE-TIME BEAT + Future.delayed(const Duration(seconds: 2), () { + if (mounted) Navigator.of(context).pop(); + }); + } + } + }); + } + + void startAnimations() { + progressController.repeat(); + startPulseAnimation(); + } + + void startPulseAnimation() { + ringController.forward().then((_) { + ringController.reset(); + if (!showResult) startPulseAnimation(); + }); + } + + void startErrorPulse() { + errorPulseController.forward().then((_) { + errorPulseController.reset(); + if (showResult && !widget.isSuccess) startErrorPulse(); + }); + } + + @override + void dispose() { + ringController.dispose(); + progressController.dispose(); + errorPulseController.dispose(); + successPulseController.dispose(); + super.dispose(); + } + + Widget _buildIcon() { + if (!showResult) { + return SvgPicture.asset( + "assets/images/finger_print.svg", + key: const ValueKey("fingerprint"), + width: 70, + height: 70, + ); + } else if (widget.isSuccess) { + return SizedBox( + width: 120, + height: 120, + child: Image.asset( + "assets/images/tick.png", + key: const ValueKey("success"), + fit: BoxFit.contain, + ), + ); + } else { + return SizedBox( + width: 120, + height: 120, + child: Image.asset( + "assets/images/error.png", + key: const ValueKey('error'), + fit: BoxFit.contain, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AppBackground( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج", + style: const TextStyle( + color: Colors.white, + fontSize: 30, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + + const SizedBox(height: 100), + + Container( + width: 280, + height: 400, + decoration: BoxDecoration( + color: const Color(0xFFEEFFFA), + borderRadius: BorderRadius.circular(38), + boxShadow: [ + BoxShadow( + color: const Color(0x34000000), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 160, + height: 160, + child: Stack( + alignment: Alignment.center, + children: [ + // GREY CIRCLE (base) + Container( + width: 120, + height: 120, + decoration: const BoxDecoration( + color: Color(0xFFE5E5E5), + shape: BoxShape.circle, + ), + ), + + // MAIN ICON SWITCHER + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: _buildIcon(), + ), + + // LOADING MODE + if (!showResult) + Positioned.fill( + child: AnimatedBuilder( + animation: progressAnimation, + builder: + (_, __) => CustomPaint( + painter: _ProgressRingPainter( + progress: progressAnimation.value, + ), + ), + ), + ), + if (!showResult) + Positioned.fill( + child: AnimatedBuilder( + animation: ringAnimation, + builder: + (_, __) => CustomPaint( + painter: _PulseRingsPainter( + progress: ringAnimation.value, + ), + ), + ), + ), + + // ERROR ANIMATION + if (showResult && !widget.isSuccess) + Positioned.fill( + child: AnimatedBuilder( + animation: errorPulseAnimation, + builder: + (_, __) => CustomPaint( + painter: _ErrorPulsePainter( + progress: errorPulseAnimation.value, + ), + ), + ), + ), + + // SUCCESS ONE-TIME PULSE + if (showResult && widget.isSuccess) + Positioned.fill( + child: AnimatedBuilder( + animation: successPulseAnimation, + builder: + (_, __) => CustomPaint( + painter: _SuccessPulsePainter( + progress: successPulseAnimation.value, + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 40), + + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: + showResult + ? Text( + widget.isSuccess + ? (widget.isLogin + ? "تم تسجيل دخولك بنجاح" + : "تم تسجيل خروجك بنجاح") + : "تم رفض تسجيل الدخول", + key: ValueKey("text_${widget.isSuccess}"), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ) + : Text( + widget.isLogin + ? "يتم تسجيل الدخول..." + : "يتم تسجيل الخروج...", + key: const ValueKey("loading_text"), + style: const TextStyle( + fontSize: 16, + decoration: TextDecoration.none, + ), + ), + ), + + // RETRY BUTTON + if (showResult && !widget.isSuccess) ...[ + const SizedBox(height: 20), + GestureDetector( + onTap: () { + setState(() => showResult = false); + + errorPulseController.stop(); + successPulseController.reset(); + startAnimations(); + + Future.delayed(const Duration(seconds: 2), () { + if (!mounted) return; + + setState(() => showResult = true); + + ringController.stop(); + progressController.stop(); + + if (!widget.isSuccess) { + startErrorPulse(); + } else { + successPulseController.forward(); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) Navigator.of(context).pop(); + }); + } + }); + }, + child: const Text( + "أعد المحاولة", + style: TextStyle( + color: Color(0xFFB00020), + fontSize: 16, + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +// ------------------------- PAINTERS -------------------------- // + +class _ProgressRingPainter extends CustomPainter { + final double progress; + _ProgressRingPainter({required this.progress}); + + @override + void paint(Canvas canvas, Size size) { + final center = size.center(Offset.zero); + final radius = 50.0; + + final bgPaint = + Paint() + ..color = const Color(0x0032C599) + ..style = PaintingStyle.stroke + ..strokeWidth = 3; + + canvas.drawCircle(center, radius, bgPaint); + + final fgPaint = + Paint() + ..color = const Color(0xC40A4433) + ..style = PaintingStyle.stroke + ..strokeWidth = 3 + ..strokeCap = StrokeCap.round; + + final sweep = 2 * 3.1415926 * progress; + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + -1.5708, + sweep, + false, + fgPaint, + ); + } + + @override + bool shouldRepaint(_) => true; +} + +class _PulseRingsPainter extends CustomPainter { + final double progress; + _PulseRingsPainter({required this.progress}); + + @override + void paint(Canvas canvas, Size size) { + final center = size.center(Offset.zero); + const baseRadius = 60.0; + const maxRadius = 130.0; + + for (final phase in [0.0, 0.25, 0.5]) { + final rp = (progress - phase).clamp(0.0, 1.0); + if (rp > 0) { + final radius = baseRadius + (maxRadius - baseRadius) * rp; + final opacity = (1 - rp) * 0.45; + + final paint = + Paint() + ..color = const Color(0xFF32C59A).withOpacity(opacity) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + canvas.drawCircle(center, radius, paint); + } + } + } + + @override + bool shouldRepaint(_) => true; +} + +// ERROR PAINTER +class _ErrorPulsePainter extends CustomPainter { + final double progress; + _ErrorPulsePainter({required this.progress}); + + @override + void paint(Canvas canvas, Size size) { + final center = size.center(Offset.zero); + + // static ring + final staticPaint = + Paint() + ..color = const Color(0xFFB00020).withOpacity(0.20) + ..style = PaintingStyle.stroke + ..strokeWidth = 3; + + canvas.drawCircle(center, 70, staticPaint); + + // pulse ring + final radius = 70 + (20 * progress); + final opacity = (1 - progress) * 0.45; + + final pulsePaint = + Paint() + ..color = const Color(0xFFB00020).withOpacity(opacity) + ..style = PaintingStyle.stroke + ..strokeWidth = 3; + + canvas.drawCircle(center, radius, pulsePaint); + } + + @override + bool shouldRepaint(_) => true; +} + +// SUCCESS one-time pulse +class _SuccessPulsePainter extends CustomPainter { + final double progress; + _SuccessPulsePainter({required this.progress}); + + @override + void paint(Canvas canvas, Size size) { + final center = size.center(Offset.zero); + + final radius = 70 + (20 * progress); + final opacity = (1 - progress) * 0.40; + + final paint = + Paint() + ..color = const Color(0xFF32C59A).withOpacity(opacity) + ..style = PaintingStyle.stroke + ..strokeWidth = 3; + + canvas.drawCircle(center, radius, paint); + } + + @override + bool shouldRepaint(_) => true; +} diff --git a/lib/widgets/login_animation_screen.dart b/lib/widgets/login_animation_screen.dart deleted file mode 100644 index 201a697..0000000 --- a/lib/widgets/login_animation_screen.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import '../widgets/app_background.dart'; - -class LoginAnimationScreen extends StatefulWidget { - final bool isLogin; - final bool isSuccess; - - const LoginAnimationScreen({ - super.key, - required this.isLogin, - required this.isSuccess, - }); - - @override - State createState() => _LoginAnimationScreenState(); -} - -class _LoginAnimationScreenState extends State - with TickerProviderStateMixin { - late AnimationController ringController; - late AnimationController progressController; - late Animation ringAnimation; - late Animation progressAnimation; - - bool showResult = false; - - @override - void initState() { - super.initState(); - - ringController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - - progressController = AnimationController( - vsync: this, - duration: const Duration(seconds: 2), - ); - - ringAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation(parent: ringController, curve: Curves.easeOut)); - - progressAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: progressController, curve: Curves.linear), - ); - - startAnimations(); - - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - showResult = true; - }); - - ringController.stop(); - progressController.stop(); - - if (widget.isSuccess) { - Future.delayed(const Duration(seconds: 2), () { - if (mounted) Navigator.of(context).pop(); - }); - } - } - }); - } - - void startAnimations() { - progressController.repeat(); - startPulseAnimation(); - } - - void startPulseAnimation() { - ringController.forward().then((_) { - ringController.reset(); - if (!showResult) startPulseAnimation(); - }); - } - - @override - void dispose() { - ringController.dispose(); - progressController.dispose(); - super.dispose(); - } - - // FIXED: Simplified _buildIcon method that returns the appropriate SVG - Widget _buildIcon() { - if (!showResult) { - // Loading state - fingerprint - return SvgPicture.asset( - "assets/images/finger_print.svg", - key: const ValueKey('fingerprint'), - width: 70, - height: 70, - placeholderBuilder: (context) => Icon( - Icons.fingerprint, - size: 70, - color: const Color(0xFF102D25), - ), - ); - } else if (widget.isSuccess) { - // Success state - tick mark - return Image.asset( - "assets/images/tick.png", - key: const ValueKey('success'), - width: 120, - height: 120, - fit: BoxFit.contain, - // placeholderBuilder: (context) => Icon( - // Icons.check_circle, - // size: 70, - // color: const Color(0xFF32C59A), - // ), - ); - } else { - // Error state - error icon - return Image.asset( - "assets/images/error.png", - key: const ValueKey('error'), - width: 120, - height: 120, - fit: BoxFit.contain, - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: AppBackground( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج", - style: const TextStyle( - color: Colors.white, - fontSize: 30, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - - const SizedBox(height: 100), - - Container( - width: 280, - height: 400, - decoration: BoxDecoration( - color: const Color(0xFFEEFFFA), - borderRadius: BorderRadius.circular(38), - boxShadow: [ - BoxShadow( - color: const Color(0x34000000), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 160, - height: 160, - child: Stack( - alignment: Alignment.center, - children: [ - Container( - width: 120, - height: 120, - decoration: const BoxDecoration( - color: Color(0xFFE5E5E5), - shape: BoxShape.circle, - ), - ), - - // FIXED: AnimatedSwitcher with proper key handling - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: _buildIcon(), - layoutBuilder: (currentChild, previousChildren) { - return Stack( - alignment: Alignment.center, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], - ); - }, - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: ScaleTransition( - scale: animation, - child: child, - ), - ); - }, - ), - - if (!showResult) - Positioned.fill( - child: AnimatedBuilder( - animation: progressAnimation, - builder: (_, __) { - return CustomPaint( - painter: _ProgressRingPainter( - progress: progressAnimation.value, - ), - ); - }, - ), - ), - - if (!showResult) - Positioned.fill( - child: AnimatedBuilder( - animation: ringAnimation, - builder: (_, __) { - return CustomPaint( - painter: _PulseRingsPainter( - progress: ringAnimation.value, - ), - ); - }, - ), - ), - ], - ), - ), - - const SizedBox(height: 40), - - // Text AnimatedSwitcher with proper key handling - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: showResult - ? Text( - widget.isSuccess - ? "تم تسجيل دخولك بنجاح" - : "تم رفض تسجيل الدخول ", - key: ValueKey("text_${widget.isSuccess}"), - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w600, - decoration: TextDecoration.none, - ), - ) - : const Text( - "يتم تسجيل الدخول ...", - key: ValueKey("loading_text"), - style: TextStyle( - fontSize: 16, - decoration: TextDecoration.none, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} - -class _ProgressRingPainter extends CustomPainter { - final double progress; - - _ProgressRingPainter({required this.progress}); - - @override - void paint(Canvas canvas, Size size) { - final center = size.center(Offset.zero); - - // radius slightly bigger than gray circle (60) - final radius = 50.0; - - // background circle (very subtle) - final bgPaint = Paint() - ..color = const Color(0x0032C599) - ..style = PaintingStyle.stroke - ..strokeWidth = 3.0; - - canvas.drawCircle(center, radius, bgPaint); - - // foreground arc that animates - final progressPaint = Paint() - ..color = const Color(0xC40A4433) - ..style = PaintingStyle.stroke - ..strokeWidth = 3.0 - ..strokeCap = StrokeCap.round; - - const startAngle = -1.5708; // -90° in radians - final sweepAngle = 2 * 3.1415926 * progress; - - canvas.drawArc( - Rect.fromCircle(center: center, radius: radius), - startAngle, - sweepAngle, - false, - progressPaint, - ); - } - - @override - bool shouldRepaint(_ProgressRingPainter oldDelegate) => - oldDelegate.progress != progress; -} - -class _PulseRingsPainter extends CustomPainter { - final double progress; - - _PulseRingsPainter({required this.progress}); - - @override - void paint(Canvas canvas, Size size) { - final center = size.center(Offset.zero); - - final baseRadius = 60.0; // start at grey circle - final maxRadius = 130.0; // outermost ripple - - final ringPhases = [0.0, 0.25, 0.5]; - - for (final phase in ringPhases) { - final ringProgress = (progress - phase).clamp(0.0, 1.0); - - if (ringProgress > 0) { - final radius = baseRadius + (maxRadius - baseRadius) * ringProgress; - final opacity = (1.0 - ringProgress) * 0.45; - - final paint = Paint() - ..color = const Color(0xFF32C59A).withOpacity(opacity) - ..style = PaintingStyle.stroke - ..strokeWidth = 2; - - canvas.drawCircle(center, radius, paint); - } - } - } - - @override - bool shouldRepaint(_PulseRingsPainter oldDelegate) => - oldDelegate.progress != progress; -} \ No newline at end of file diff --git a/lib/widgets/settings_bar.dart b/lib/widgets/settings_bar.dart index ea38677..26dc700 100644 --- a/lib/widgets/settings_bar.dart +++ b/lib/widgets/settings_bar.dart @@ -12,7 +12,7 @@ class SettingsBar extends StatelessWidget { super.key, required this.selectedIndex, required this.onTap, - this.showBackButton = false, + this.showBackButton = false, //to swicth between back button and settings icons this.onBackTap, required this.iconPaths, }); @@ -21,24 +21,18 @@ class SettingsBar extends StatelessWidget { Widget build(BuildContext context) { return Container( color: Colors.transparent, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Image.asset( - 'assets/images/logo2.png', - width: 150, - height: 40, - ), + Image.asset('assets/images/logo2.png', width: 150, height: 40), ], ), - - // Navigation icons on the right Row( children: [ - // Back button (only shown when showBackButton is true) + if (showBackButton) GestureDetector( onTap: onBackTap, @@ -56,71 +50,56 @@ class SettingsBar extends StatelessWidget { ), ], ), - child: const Center( - child: Icon( - Icons.arrow_back, - color: Color(0xFF006838), + child: Center( + child: SvgPicture.asset( + "assets/images/back.svg", + width: 26, + height: 26, ), ), ), ), - - // Add spacing if back button is shown - if (showBackButton) const SizedBox(width: 20), - - // Settings and notification icons - ...iconPaths.asMap().entries.map((entry) { - final index = entry.key; - final iconPath = entry.value; - final isSelected = selectedIndex == index; - return Padding( - padding: const EdgeInsets.only(left: 10), - child: GestureDetector( - onTap: () => onTap(index), - child: Container( - width: 43, - height: 43, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: const Color(0x10000000), - blurRadius: 5, - offset: const Offset(0, 2), - ), - ], - ), - child: Center( - child: Stack( - children: [ - SvgPicture.asset( - iconPath, - width: 30, - height: 30, - + // When back button is OFF → show user + settings icons + if (!showBackButton) + ...iconPaths.asMap().entries.map((entry) { + final index = entry.key; + final iconPath = entry.value; + // final isSelected = selectedIndex == index; + + return Padding( + padding: const EdgeInsets.only(left: 10), + child: GestureDetector( + onTap: () => onTap(index), + child: Container( + width: 43, + height: 43, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color(0x10000000), + blurRadius: 5, + offset: const Offset(0, 2), ), - if (index == 1) - Positioned( - top: 0, - right: 0, - child: Container( - width: 10, - height: 10, - ), - ), ], ), + child: Center( + child: Stack( + children: [ + SvgPicture.asset(iconPath, width: 30, height: 30), + ], + ), + ), ), ), - ), - ); - }).toList(), + ); + }), ], ), ], ), ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index e42a4d5..ed985c9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" boolean_selector: dependency: transitive description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -172,10 +172,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -361,10 +361,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: