diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index a0625e6..fee81e6 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -36,6 +36,10 @@ "offers": "Offers & ", "discounts": "Discounts", "newString": "New", + "setTheNewPassword": "Set the new password", + "typeYourNewPasswordBelow": "Type your new password below", + "confirmPassword": "Confirm Password", + "update": "Update", "title": "Title", "home": "Home", "mySalary": "My Salary", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index a0625e6..fee81e6 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -36,6 +36,10 @@ "offers": "Offers & ", "discounts": "Discounts", "newString": "New", + "setTheNewPassword": "Set the new password", + "typeYourNewPasswordBelow": "Type your new password below", + "confirmPassword": "Confirm Password", + "update": "Update", "title": "Title", "home": "Home", "mySalary": "My Salary", diff --git a/lib/classes/colors.dart b/lib/classes/colors.dart index d9c5656..6820e7d 100644 --- a/lib/classes/colors.dart +++ b/lib/classes/colors.dart @@ -11,12 +11,16 @@ class MyColors { static const Color backgroundColor = Color(0xffF8F8F8); static const Color grey57Color = Color(0xff575757); static const Color grey77Color = Color(0xff777777); + static const Color grey70Color = Color(0xff707070); + static const Color greyACColor = Color(0xffACACAC); static const Color grey98Color = Color(0xff989898); static const Color lightGreyEFColor = Color(0xffEFEFEF); static const Color lightGreyEDColor = Color(0xffEDEDED); + static const Color lightGreyEAColor = Color(0xffEAEAEA); static const Color darkWhiteColor = Color(0xffE0E0E0); static const Color redColor = Color(0xffD02127); static const Color yellowColor = Color(0xffF4E31C); + static const Color backgroundBlackColor = Color(0xff202529); static const Color black = Color(0xff000000); static const Color white = Color(0xffffffff); static const Color green = Color(0xffffffff); diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 7d171b7..4e72b26 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:mohem_flutter_app/ui/dashboard.dart'; +import 'package:mohem_flutter_app/ui/landing/dashboard.dart'; +import 'package:mohem_flutter_app/ui/landing/today_attendance_screen.dart'; import 'package:mohem_flutter_app/ui/login/login_screen.dart'; +import 'package:mohem_flutter_app/ui/login/new_password_screen.dart'; import 'package:mohem_flutter_app/ui/login/verify_login_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/missing_swipe/missing_swipe_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/work_list_screen.dart'; @@ -11,9 +13,10 @@ class AppRoutes { static const String loginVerifyAccount = "/loginVerifyAccount"; static const String login = "/login"; static const String verifyLogin = "/verifyLogin"; - static const String forgetPassword = "/forgetPassword"; + static const String newPassword = "/newPassword"; static const String loginVerification = "/loginVerification"; static const String dashboard = "/dashboard"; + static const String todayAttendance = "/todayAttendance"; static const String initialRoute = login; //Work List @@ -24,6 +27,8 @@ class AppRoutes { login: (context) => LoginScreen(), verifyLogin: (context) => VerifyLoginScreen(), dashboard: (context) => Dashboard(), + newPassword: (context) => NewPasswordScreen(), + todayAttendance: (context) => TodayAttendanceScreen(), //Work List workList: (context) => WorkListScreen(), diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 21e370a..9db4829 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -7,16 +7,10 @@ extension EmailValidator on String { Widget toText10({Color? color, bool isBold = false}) => Text( this, - style: TextStyle( - fontSize: 10, - fontWeight: isBold ? FontWeight.bold : FontWeight.w600, - color: color ?? MyColors.darkTextColor, - letterSpacing: -0.4), + style: TextStyle(fontSize: 10, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.4), ); - Widget toText11( - {Color? color, bool isUnderLine = false, bool isBold = false}) => - Text( + Widget toText11({Color? color, bool isUnderLine = false, bool isBold = false}) => Text( this, style: TextStyle( fontSize: 11, @@ -26,13 +20,7 @@ extension EmailValidator on String { decoration: isUnderLine ? TextDecoration.underline : null), ); - Widget toText12( - {Color? color, - bool isUnderLine = false, - bool isBold = false, - bool isCenter = false, - int maxLine = 0}) => - Text( + Widget toText12({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0}) => Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: (maxLine > 0) ? maxLine : null, @@ -46,46 +34,41 @@ extension EmailValidator on String { Widget toText13({Color? color, bool isUnderLine = false}) => Text( this, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: color ?? MyColors.darkTextColor, - letterSpacing: -0.52, - decoration: isUnderLine ? TextDecoration.underline : null), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.52, decoration: isUnderLine ? TextDecoration.underline : null), ); Widget toText14({Color? color, bool isBold = false}) => Text( this, - style: TextStyle( - color: color ?? MyColors.darkTextColor, - fontSize: 14, - letterSpacing: -0.48, - fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + style: TextStyle(color: color ?? MyColors.darkTextColor, fontSize: 14, letterSpacing: -0.48, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), ); Widget toText16({Color? color, bool isBold = false}) => Text( this, - style: TextStyle( - color: color ?? MyColors.darkTextColor, - fontSize: 16, - letterSpacing: -0.64, - fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + style: TextStyle(color: color ?? MyColors.darkTextColor, fontSize: 16, letterSpacing: -0.64, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), ); - Widget toText24({bool isBold = false, Color? textColor}) => Text( + Widget toText17({Color? color, bool isBold = false}) => Text( this, - style: TextStyle( - height: 23 / 24, - color: textColor ?? MyColors.darkTextColor, - fontSize: 24, - letterSpacing: -1.44, - fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + style: TextStyle(color: color ?? MyColors.darkTextColor, fontSize: 17, letterSpacing: -0.68, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText22({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 1, color: color ?? MyColors.darkTextColor, fontSize: 22, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText24({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 23 / 24, color: color ?? MyColors.darkTextColor, fontSize: 24, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText32({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 32, letterSpacing: -1.92, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), ); bool isValidEmail() { - return RegExp( - r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$') - .hasMatch(this); + return RegExp(r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$').hasMatch(this); } String toFormattedDate() { diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index a471169..9829f4c 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -52,6 +52,10 @@ class CodegenLoader extends AssetLoader{ "offers": "Offers & ", "discounts": "Discounts", "newString": "New", + "setTheNewPassword": "Set the new password", + "typeYourNewPasswordBelow": "Type your new password below", + "confirmPassword": "Confirm Password", + "update": "Update", "title": "Title", "home": "Home", "mySalary": "My Salary", @@ -61,6 +65,8 @@ class CodegenLoader extends AssetLoader{ "msg": "Hello {} in the {} world ", "msg_named": "{} are written in the {lang} language", "clickMe": "Click me", + "human": "Human", + "resources": "Resources", "profile": { "reset_password": { "label": "Reset Password", @@ -132,6 +138,10 @@ static const Map en_US = { "offers": "Offers & ", "discounts": "Discounts", "newString": "New", + "setTheNewPassword": "Set the new password", + "typeYourNewPasswordBelow": "Type your new password below", + "confirmPassword": "Confirm Password", + "update": "Update", "title": "Title", "home": "Home", "mySalary": "My Salary", @@ -141,6 +151,8 @@ static const Map en_US = { "msg": "Hello {} in the {} world ", "msg_named": "{} are written in the {lang} language", "clickMe": "Click me", + "human": "Human", + "resources": "Resources", "profile": { "reset_password": { "label": "Reset Password", diff --git a/lib/ui/app_bar.dart b/lib/ui/app_bar.dart index 0823a5a..c39e249 100644 --- a/lib/ui/app_bar.dart +++ b/lib/ui/app_bar.dart @@ -6,10 +6,11 @@ import 'package:mohem_flutter_app/extensions/string_extensions.dart'; AppBar appBar(BuildContext context, {required String title}) { return AppBar( - title: title.toText24(textColor: MyColors.darkTextColor), + title: title.toText24(color: MyColors.darkTextColor), centerTitle: false, automaticallyImplyLeading: false, backgroundColor: Colors.white, + actions: [ IconButton( onPressed: () { diff --git a/lib/ui/dashboard.dart b/lib/ui/landing/dashboard.dart similarity index 66% rename from lib/ui/dashboard.dart rename to lib/ui/landing/dashboard.dart index 91a6e4f..795460d 100644 --- a/lib/ui/dashboard.dart +++ b/lib/ui/landing/dashboard.dart @@ -32,34 +32,14 @@ class _DashboardState extends State { @override Widget build(BuildContext context) { - List names = [ - LocaleKeys.workList.tr(), - LocaleKeys.missingSwipes.tr(), - LocaleKeys.leaveBalance.tr(), - LocaleKeys.ticketBalance.tr() - ]; + List names = [LocaleKeys.workList.tr(), LocaleKeys.missingSwipes.tr(), LocaleKeys.leaveBalance.tr(), LocaleKeys.ticketBalance.tr()]; List namesInt = [118, 02, 18.5, 03]; List namesColor = [0xff125765, 0xff239D8F, 0xff2BB8A8, 0xff1D92AA]; - List namesT = [ - LocaleKeys.monthlyAttendance.tr(), - LocaleKeys.workFromHome.tr(), - LocaleKeys.ticketRequest.tr(), - LocaleKeys.monthlyAttendance.tr() - ]; - List iconT = [ - "assets/images/monthly_attendance.svg", - "assets/images/work_from_home.svg", - "assets/images/ticket_request.svg", - "assets/images/work_from_home.svg" - ]; + List namesT = [LocaleKeys.monthlyAttendance.tr(), LocaleKeys.workFromHome.tr(), LocaleKeys.ticketRequest.tr(), LocaleKeys.monthlyAttendance.tr()]; + List iconT = ["assets/images/monthly_attendance.svg", "assets/images/work_from_home.svg", "assets/images/ticket_request.svg", "assets/images/work_from_home.svg"]; - List namesD = [ - "Nostalgia Perfume Perfume", - "Al Nafoura", - "AlJadi", - "Nostalgia Perfume" - ]; + List namesD = ["Nostalgia Perfume Perfume", "Al Nafoura", "AlJadi", "Nostalgia Perfume"]; return Scaffold( body: Column( @@ -72,8 +52,7 @@ class _DashboardState extends State { CircularAvatar( width: 34, height: 34, - url: - "https://cdn4.iconfinder.com/data/icons/professions-2-2/151/89-512.png", + url: "https://cdn4.iconfinder.com/data/icons/professions-2-2/151/89-512.png", ), 8.width, SvgPicture.asset("assets/images/side_nav.svg"), @@ -103,9 +82,7 @@ class _DashboardState extends State { top: 0, child: Container( padding: const EdgeInsets.only(left: 5, right: 5), - decoration: BoxDecoration( - color: MyColors.redColor, - borderRadius: BorderRadius.circular(17)), + decoration: BoxDecoration(color: MyColors.redColor, borderRadius: BorderRadius.circular(17)), child: "3".toText12(color: Colors.white), ), ) @@ -121,9 +98,7 @@ class _DashboardState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.goodMorning - .tr() - .toText14(color: MyColors.grey77Color), + LocaleKeys.goodMorning.tr().toText14(color: MyColors.grey77Color), "Mahmoud Shrouf".toText24(isBold: true), 16.height, Row( @@ -134,41 +109,27 @@ class _DashboardState extends State { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), - gradient: const LinearGradient( - transform: GradientRotation(.46), - begin: Alignment.topRight, - end: Alignment.bottomRight, - colors: [ - MyColors.gradiantEndColor, - MyColors.gradiantStartColor, - ]), + gradient: const LinearGradient(transform: GradientRotation(.46), begin: Alignment.topRight, end: Alignment.bottomRight, colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ]), ), child: Stack( alignment: Alignment.center, children: [ SvgPicture.asset("assets/images/"), Column( - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.markAttendance - .tr() - .toText14( - color: Colors.white, - isBold: true), + LocaleKeys.markAttendance.tr().toText14(color: Colors.white, isBold: true), 9.height, - "07:55:12".toText14( - color: Colors.white, - isBold: true), - LocaleKeys.timeLeftToday - .tr() - .toText12(color: Colors.white), + "07:55:12".toText14(color: Colors.white, isBold: true), + LocaleKeys.timeLeftToday.tr().toText12(color: Colors.white), 9.height, const ClipRRect( borderRadius: BorderRadius.all( @@ -177,50 +138,33 @@ class _DashboardState extends State { child: LinearProgressIndicator( value: 0.7, minHeight: 8, - valueColor: - const AlwaysStoppedAnimation< - Color>(Colors.white), - backgroundColor: - const Color(0xff196D73), + valueColor: const AlwaysStoppedAnimation(Colors.white), + backgroundColor: const Color(0xff196D73), ), ), ], - ).paddingOnly( - top: 12, right: 15, left: 12), + ).paddingOnly(top: 12, right: 15, left: 12), ), Row( children: [ Expanded( child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - LocaleKeys.checkIn - .tr() - .toText12( - color: Colors.white), - "09:00".toText14( - color: Colors.white, - isBold: true), - 4.height - ], + crossAxisAlignment: CrossAxisAlignment.start, + children: [LocaleKeys.checkIn.tr().toText12(color: Colors.white), "09:00".toText14(color: Colors.white, isBold: true), 4.height], ).paddingOnly(left: 12), ), Container( width: 45, height: 45, - padding: const EdgeInsets.only( - left: 14, right: 14), + padding: const EdgeInsets.only(left: 14, right: 14), decoration: const BoxDecoration( color: Color(0xff259EA4), borderRadius: BorderRadius.only( - bottomRight: - Radius.circular(15), + bottomRight: Radius.circular(15), ), ), - child: SvgPicture.asset( - "assets/images/stop.svg"), + child: SvgPicture.asset("assets/images/stop.svg"), ), ], ), @@ -228,7 +172,9 @@ class _DashboardState extends State { ), ], ), - ), + ).onPress(() { + Navigator.pushNamed(context, AppRoutes.todayAttendance); + }), ), ), 9.width, @@ -237,12 +183,7 @@ class _DashboardState extends State { shrinkWrap: true, primary: false, physics: const NeverScrollableScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 2 / 2, - crossAxisSpacing: 9, - mainAxisSpacing: 9), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 2 / 2, crossAxisSpacing: 9, mainAxisSpacing: 9), padding: EdgeInsets.zero, itemCount: 4, itemBuilder: (BuildContext context, int index) { @@ -252,31 +193,22 @@ class _DashboardState extends State { borderRadius: BorderRadius.circular(10), ), child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ names[index].toText12(color: Colors.white), Row( children: [ Expanded( - child: namesInt[index] - .toStringAsFixed(1) - .toText16( - color: Colors.white, - isBold: true), + child: namesInt[index].toStringAsFixed(1).toText16(color: Colors.white, isBold: true), ), - SvgPicture.asset( - "assets/images/arrow_next.svg", - color: Colors.white) + SvgPicture.asset("assets/images/arrow_next.svg", color: Colors.white) ], ) ], - ).paddingOnly( - left: 10, right: 10, bottom: 6, top: 6), + ).paddingOnly(left: 10, right: 10, bottom: 6, top: 6), ).onPress(() { - Navigator.pushNamed( - context, AppRoutes.workList); + Navigator.pushNamed(context, AppRoutes.workList); }); }, ), @@ -292,14 +224,12 @@ class _DashboardState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - "Other".toText12(), + LocaleKeys.other.tr().toText12(), LocaleKeys.services.tr().toText24(isBold: true), ], ), ), - LocaleKeys.viewAllServices - .tr() - .toText12(isUnderLine: true), + LocaleKeys.viewAllServices.tr().toText12(isUnderLine: true), ], ), ], @@ -309,8 +239,7 @@ class _DashboardState extends State { child: ListView.separated( shrinkWrap: true, physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only( - left: 21, right: 21, top: 13, bottom: 13), + padding: const EdgeInsets.only(left: 21, right: 21, top: 13, bottom: 13), scrollDirection: Axis.horizontal, itemBuilder: (cxt, index) { return AspectRatio( @@ -321,8 +250,7 @@ class _DashboardState extends State { borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( - color: - const Color(0xff000000).withOpacity(.05), + color: const Color(0xff000000).withOpacity(.05), blurRadius: 26, offset: const Offset(0, -3), ), @@ -337,17 +265,13 @@ class _DashboardState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( - child: - namesT[index].toText11(isBold: true), + child: namesT[index].toText11(isBold: true), ), - SvgPicture.asset( - "assets/images/arrow_next.svg") - .paddingOnly(bottom: 4) + SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4) ], ) ], - ).paddingOnly( - left: 10, right: 10, bottom: 10, top: 12), + ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12), ), ); }, @@ -360,11 +284,8 @@ class _DashboardState extends State { padding: EdgeInsets.only(top: 31), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.only( - topRight: Radius.circular(50), - topLeft: Radius.circular(50)), - border: - Border.all(color: MyColors.lightGreyEDColor, width: 1), + borderRadius: BorderRadius.only(topRight: Radius.circular(50), topLeft: Radius.circular(50)), + border: Border.all(color: MyColors.lightGreyEDColor, width: 1), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -380,29 +301,21 @@ class _DashboardState extends State { LocaleKeys.offers.tr().toText12(), Row( children: [ - LocaleKeys.discounts - .tr() - .toText24(isBold: true), + LocaleKeys.discounts.tr().toText24(isBold: true), 6.width, Container( - padding: const EdgeInsets.only( - left: 8, right: 8), + padding: const EdgeInsets.only(left: 8, right: 8), decoration: BoxDecoration( color: MyColors.yellowColor, - borderRadius: - BorderRadius.circular(10), + borderRadius: BorderRadius.circular(10), ), - child: LocaleKeys.newString - .tr() - .toText10(isBold: true)), + child: LocaleKeys.newString.tr().toText10(isBold: true)), ], ), ], ), ), - LocaleKeys.viewAllOffers - .tr() - .toText12(isUnderLine: true), + LocaleKeys.viewAllOffers.tr().toText12(isUnderLine: true), ], ).paddingOnly(left: 21, right: 21), SizedBox( @@ -410,8 +323,7 @@ class _DashboardState extends State { child: ListView.separated( shrinkWrap: true, physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only( - left: 21, right: 21, top: 13), + padding: const EdgeInsets.only(left: 21, right: 21, top: 13), scrollDirection: Axis.horizontal, itemBuilder: (cxt, index) { return SizedBox( @@ -426,9 +338,7 @@ class _DashboardState extends State { borderRadius: const BorderRadius.all( Radius.circular(100), ), - border: Border.all( - color: MyColors.lightGreyEDColor, - width: 1), + border: Border.all(color: MyColors.lightGreyEDColor, width: 1), ), child: ClipRRect( borderRadius: const BorderRadius.all( @@ -441,9 +351,7 @@ class _DashboardState extends State { ), ), 4.height, - Expanded( - child: namesD[6 % (index + 1)].toText12( - isCenter: true, maxLine: 2)), + Expanded(child: namesD[6 % (index + 1)].toText12(isCenter: true, maxLine: 2)), ], ), ); diff --git a/lib/ui/landing/today_attendance_screen.dart b/lib/ui/landing/today_attendance_screen.dart new file mode 100644 index 0000000..0ba3791 --- /dev/null +++ b/lib/ui/landing/today_attendance_screen.dart @@ -0,0 +1,176 @@ +import 'package:easy_localization/src/public_ext.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/circular_step_progress_bar.dart'; + +class TodayAttendanceScreen extends StatefulWidget { + TodayAttendanceScreen({Key? key}) : super(key: key); + + @override + _TodayAttendanceScreenState createState() { + return _TodayAttendanceScreenState(); + } +} + +class _TodayAttendanceScreenState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: MyColors.backgroundBlackColor, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + backgroundColor: Colors.white, + body: ListView( + children: [ + Container( + color: MyColors.backgroundBlackColor, + padding: EdgeInsets.only(left: 21, right: 21, bottom: 21), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "June 13, 2021".toText24(isBold: true, color: Colors.white), + LocaleKeys.timeLeftToday.tr().toText16(color: Color(0xffACACAC)), + 21.height, + Center( + child: CircularStepProgressBar( + totalSteps: 16 * 4, + currentStep: 16, + width: 216, + height: 216, + selectedColor: MyColors.gradiantEndColor, + unselectedColor: MyColors.grey70Color, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + "08:58:15".toText32(color: Colors.white, isBold: true), + 19.height, + "Shift Time".tr().toText12(color: MyColors.greyACColor), + "08:00 - 17:00".toText22(color: Colors.white, isBold: true), + ], + ), + ), + ), + ), + ], + ), + ), + Container( + color: MyColors.backgroundBlackColor, + child: Stack( + children: [ + Container( + height: 187, + padding: EdgeInsets.only(left: 31, right: 31, top: 31, bottom: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), + gradient: const LinearGradient(transform: GradientRotation(.64), begin: Alignment.topRight, end: Alignment.bottomRight, colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ]), + ), + child: Column( + children: [ + Row( + children: [commonStatusView("Check In", "09:27"), commonStatusView("Check Out", "- - : - -")], + ), + 21.height, + Row( + children: [commonStatusView("Late In", "00:27"), commonStatusView("Regular", "08:00")], + ), + ], + ), + ), + Container( + width: double.infinity, + decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), color: Colors.white), + margin: EdgeInsets.only(top: 187 - 31), + padding: EdgeInsets.only(left: 21, right: 21, top: 24, bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + "Mark".tr().toText12(), + "Attendance".tr().toText24(), + "Select the method to mark the attendance".tr().toText12(color: Color(0xff535353)), + 24.height, + GridView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.zero, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1 / 1, crossAxisSpacing: 8, mainAxisSpacing: 8), + children: [ + attendanceMethod("NFC", "assets/images/nfc.svg", () {}), + attendanceMethod("Wifi", "assets/images/wufu.svg", () {}), + ], + ) + ], + ), + ), + // Positioned( + // top: 187 - 21, + // child: Container( + // padding: EdgeInsets.only(left: 31, right: 31, top: 31, bottom: 16), + // decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)), color: Colors.white), + // child: Column( + // children: [ + // Row( + // children: [commonStatusView("Check In", "09:27"), commonStatusView("Check Out", "- - : - -")], + // ), + // 21.height, + // Row( + // children: [commonStatusView("Late In", "00:27"), commonStatusView("Regular", "08:00")], + // ), + // ], + // ), + // ), + // ), + ], + ), + ) + ], + ), + ); + } + + Widget attendanceMethod(String title, String image, VoidCallback onPress) => Container( + padding: const EdgeInsets.only(left: 10, right: 10, top: 14, bottom: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: const LinearGradient(transform: GradientRotation(.64), begin: Alignment.topRight, end: Alignment.bottomRight, colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ]), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Expanded(child: SvgPicture.asset(image)), title.toText17(isBold: true, color: Colors.white)], + ), + ).onPress(onPress); + + Widget commonStatusView(String title, String time) => Expanded( + child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + title.toText12(color: Colors.white), + time.toText22(color: Colors.white, isBold: true), + ]), + ); +} diff --git a/lib/ui/login/login_screen.dart b/lib/ui/login/login_screen.dart index 6e043bd..f7f8e05 100644 --- a/lib/ui/login/login_screen.dart +++ b/lib/ui/login/login_screen.dart @@ -2,13 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/src/public_ext.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:mohem_flutter_app/config/routes.dart'; -import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; -import 'package:mohem_flutter_app/widgets/input_widget.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:mohem_flutter_app/widgets/input_widget.dart'; class LoginScreen extends StatefulWidget { LoginScreen({Key? key}) : super(key: key); @@ -50,20 +51,14 @@ class _LoginScreenState extends State { Expanded(child: SizedBox()), Row( children: [ - Text( - LocaleKeys.english.tr(), - style: TextStyle(color: MyColors.textMixColor, fontSize: 14, letterSpacing: -0.48, fontWeight: FontWeight.w600), - ).onPress(() {}), + LocaleKeys.english.tr().toText14(color: MyColors.textMixColor).onPress(() {}), Container( width: 1, color: MyColors.darkWhiteColor, height: 16, margin: const EdgeInsets.only(left: 10, right: 10), ), - Text( - LocaleKeys.arabic.tr(), - style: TextStyle(color: MyColors.darkTextColor, fontSize: 14, letterSpacing: -0.48, fontWeight: FontWeight.w600), - ).onPress(() {}), + LocaleKeys.arabic.tr().toText14().onPress(() {}), ], ), ], @@ -74,18 +69,19 @@ class _LoginScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - LocaleKeys.login.tr(), - style: TextStyle(color: MyColors.darkTextColor, fontSize: 24, letterSpacing: -1.44, fontWeight: FontWeight.w700), - ), - Text( - LocaleKeys.pleaseEnterLoginDetails.tr(), - style: TextStyle(color: MyColors.darkTextColor, fontSize: 16, letterSpacing: -0.64, fontWeight: FontWeight.w600), - ), + LocaleKeys.login.tr().toText24(isBold: true), + LocaleKeys.pleaseEnterLoginDetails.tr().toText16(), 16.height, InputWidget(LocaleKeys.username.tr(), "123456", username), 12.height, - InputWidget(LocaleKeys.password.tr(), "xxxxxx", password, isObscureText: true) + InputWidget(LocaleKeys.password.tr(), "xxxxxx", password, isObscureText: true), + 9.height, + Align( + alignment: Alignment.centerRight, + child: LocaleKeys.forgotPassword.tr().toText12(isUnderLine: true, color: MyColors.textMixColor).onPress(() { + Navigator.pushNamed(context, AppRoutes.newPassword); + }), + ), ], ), ) diff --git a/lib/ui/login/new_password_screen.dart b/lib/ui/login/new_password_screen.dart new file mode 100644 index 0000000..dca855e --- /dev/null +++ b/lib/ui/login/new_password_screen.dart @@ -0,0 +1,92 @@ +import 'package:easy_localization/src/public_ext.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:mohem_flutter_app/widgets/input_widget.dart'; + +class NewPasswordScreen extends StatefulWidget { + NewPasswordScreen({Key? key}) : super(key: key); + + @override + _NewPasswordScreenState createState() { + return _NewPasswordScreenState(); + } +} + +class _NewPasswordScreenState extends State { + TextEditingController password = TextEditingController(); + TextEditingController confirmPassword = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + const SizedBox(height: 23), + Expanded( + child: Padding( + padding: const EdgeInsets.all(21.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded(child: SizedBox()), + Row( + children: [ + LocaleKeys.english.tr().toText14(color: MyColors.textMixColor).onPress(() {}), + Container( + width: 1, + color: MyColors.darkWhiteColor, + height: 16, + margin: const EdgeInsets.only(left: 10, right: 10), + ), + LocaleKeys.arabic.tr().toText14().onPress(() {}), + ], + ), + ], + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocaleKeys.setTheNewPassword.tr().toText24(isBold: true), + LocaleKeys.typeYourNewPasswordBelow.tr().toText16(), + 16.height, + InputWidget(LocaleKeys.password.tr(), "**********", password), + 12.height, + InputWidget(LocaleKeys.confirmPassword.tr(), "**********", confirmPassword, isObscureText: true) + ], + ), + ) + ], + ), + ), + ), + DefaultButton(LocaleKeys.update.tr(), () async { + // context.setLocale(const Locale("en", "US")); // to change Loacle + + Navigator.pushNamed(context, AppRoutes.verifyLogin); + }).insideContainer + ], + ), + ); + } +} diff --git a/lib/ui/work_list/missing_swipe/missing_swipe_screen.dart b/lib/ui/work_list/missing_swipe/missing_swipe_screen.dart index 850f565..4af620e 100644 --- a/lib/ui/work_list/missing_swipe/missing_swipe_screen.dart +++ b/lib/ui/work_list/missing_swipe/missing_swipe_screen.dart @@ -23,17 +23,11 @@ class MissingSwipeScreen extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20)), - gradient: LinearGradient( - transform: GradientRotation(.46), - begin: Alignment.topRight, - end: Alignment.bottomRight, - colors: [ - MyColors.gradiantEndColor, - MyColors.gradiantStartColor, - ]), + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + gradient: LinearGradient(transform: GradientRotation(.46), begin: Alignment.topRight, end: Alignment.bottomRight, colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ]), ), clipBehavior: Clip.antiAlias, child: TabBar( diff --git a/lib/ui/work_list/work_list_screen.dart b/lib/ui/work_list/work_list_screen.dart index c3de75f..06a545c 100644 --- a/lib/ui/work_list/work_list_screen.dart +++ b/lib/ui/work_list/work_list_screen.dart @@ -52,7 +52,7 @@ class _WorkListScreenState extends State { borderRadius: BorderRadius.circular(6), color: tabList[index].isSelected ? MyColors.darkIconColor - : MyColors.darkWhiteColor, + : MyColors.lightGreyEAColor, ), child: tabList[index].title.toText12( color: tabList[index].isSelected diff --git a/lib/widgets/circular_step_progress_bar.dart b/lib/widgets/circular_step_progress_bar.dart new file mode 100644 index 0000000..75724c8 --- /dev/null +++ b/lib/widgets/circular_step_progress_bar.dart @@ -0,0 +1,515 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class CircularStepProgressBar extends StatelessWidget { + /// Defines if steps grow from + /// clockwise [CircularDirection.clockwise] or + /// counterclockwise [CircularDirection.counterclockwise] + final CircularDirection circularDirection; + + /// Number of steps to underline, all the steps with + /// index <= [currentStep] will have [Color] equal to + /// [selectedColor] + /// + /// Only used when [customColor] is [null] + /// + /// Default value: 0 + final int currentStep; + + /// Total number of step of the complete indicator + final int totalSteps; + + /// Radial spacing between each step. Remember to + /// define the value in radiant units + /// + /// Default value: math.pi / 20 + final double padding; + + /// Height of the indicator's box container + final double? height; + + /// Width of the indicator's box container + final double? width; + + /// Assign a custom [Color] for each step + /// + /// Takes a [int], index of the current step starting from 0, and + /// must return a [Color] + /// + /// **NOTE**: If provided, it overrides + /// [selectedColor] and [unselectedColor] + final Color Function(int)? customColor; + + /// [Color] of the selected steps + /// + /// All the steps with index <= [currentStep] + /// + /// Default value: [Colors.blue] + final Color? selectedColor; + + /// [Color] of the unselected steps + /// + /// All the steps with index between + /// [currentStep] and [totalSteps] + /// + /// Default value: [Colors.grey] + final Color? unselectedColor; + + /// The size of a single step in the indicator + /// + /// Default value: 6.0 + final double stepSize; + + /// Specify a custom size for selected steps + /// + /// Only applicable when not custom setting (customColor, customStepSize) is defined + /// + /// This value will replace the [stepSize] only for selected steps + final double? selectedStepSize; + + /// Specify a custom size for unselected steps + /// + /// Only applicable when not custom setting (customColor, customStepSize) is defined + /// + /// This value will replace the [stepSize] only for unselected steps + final double? unselectedStepSize; + + /// Assign a custom size [double] for each step + /// + /// Function takes a [int], index of the current step starting from 0, and + /// a [bool], which tells if the step is selected based on [currentStep], + /// and must return a [double] size of the step + /// + /// **NOTE**: If provided, it overrides [stepSize] + final double Function(int, bool)? customStepSize; + + /// [Widget] contained inside the circular indicator + final Widget? child; + + /// Height of the indicator container in case no [height] parameter + /// given and parent height is [double.infinity] + /// + /// Default value: 100.0 + final double fallbackHeight; + + /// Height of the indicator container in case no [width] parameter + /// given and parent height is [double.infinity] + /// + /// Default value: 100.0 + final double fallbackWidth; + + /// Angle in radiants in which the first step of the indicator is placed. + /// The initial value is on the top of the indicator (- math.pi / 2) + /// - 0 => TOP + /// - math.pi / 2 => LEFT + /// - math.pi => BOTTOM + /// - math.pi / 2 * 3 => RIGHT + /// - math.pi / 2 => TOP (again) + final double startingAngle; + + /// Angle in radiants which represents the size of the arc used to display the indicator. + /// It allows you to draw a semi-circle instead of a full 360° (math.pi * 2) circle. + final double arcSize; + + /// Adds rounded edges at the beginning and at the end of the circular indicator + /// given a [int], index of each step, and a [bool], + /// which tells if the step is selected based on [currentStep], and must return a + /// [bool] that tells if the edges are rounded or not + /// + /// **NOTE**: For continuous circular indicators (`padding: 0`), to check if to apply + /// the rounded edges the packages uses index 0 (for the first arc painted) and + /// 1 (for the second arc painted) + /// + /// ```dart + /// // Example: Add rounded edges for all the steps + /// roundedCap: (index, _) => true + /// ``` + /// + /// ```dart + /// // Example: Add rounded edges for the selected arc of the indicator + /// roundedCap: (index, _) => index == 0, + /// padding: 0 + /// ``` + final bool Function(int, bool)? roundedCap; + + /// Adds a gradient color to the circular indicator + /// + /// **NOTE**: If provided, it overrides [selectedColor], [unselectedColor], and [customColor] + final Gradient? gradientColor; + + /// Removes the extra angle caused by [StrokeCap.round] when [roundedCap] is applied + final bool removeRoundedCapExtraAngle; + + const CircularStepProgressBar({ + required this.totalSteps, + this.child, + this.height, + this.width, + this.customColor, + this.customStepSize, + this.selectedStepSize, + this.unselectedStepSize, + this.roundedCap, + this.gradientColor, + this.circularDirection = CircularDirection.clockwise, + this.fallbackHeight = 100.0, + this.fallbackWidth = 100.0, + this.currentStep = 0, + this.selectedColor = Colors.blue, + this.unselectedColor = Colors.grey, + this.padding = math.pi / 20, + this.stepSize = 6.0, + this.startingAngle = 0, + this.arcSize = math.pi * 2, + this.removeRoundedCapExtraAngle = false, + Key? key, + }) : assert(totalSteps > 0, "Number of total steps (totalSteps) of the CircularStepProgressBar must be greater than 0"), + assert(currentStep >= 0, "Current step (currentStep) of the CircularStepProgressBar must be greater than or equal to 0"), + assert(padding >= 0.0, "Padding (padding) of the CircularStepProgressBar must be greater or equal to 0"), + super(key: key); + + @override + Widget build(BuildContext context) { + // Print warning when arcSize greater than math.pi * 2 which causes steps to overlap + if (arcSize > math.pi * 2) print("WARNING (step_progress_indicator): arcSize of CircularStepProgressBar is greater than 360° (math.pi * 2), this will cause some steps to overlap!"); + final TextDirection textDirection = Directionality.of(context); + + return LayoutBuilder( + builder: (context, constraints) => SizedBox( + // Apply fallback for both height and width + // if their value is null and no parent size limit + height: height != null + ? height + : constraints.maxHeight != double.infinity + ? constraints.maxHeight + : fallbackHeight, + width: width != null + ? width + : constraints.maxWidth != double.infinity + ? constraints.maxWidth + : fallbackWidth, + child: CustomPaint( + painter: _CircularIndicatorPainter( + totalSteps: totalSteps, + currentStep: currentStep, + customColor: customColor, + padding: padding, + circularDirection: circularDirection, + selectedColor: selectedColor, + unselectedColor: unselectedColor, + arcSize: arcSize, + stepSize: stepSize, + customStepSize: customStepSize, + maxDefinedSize: maxDefinedSize, + selectedStepSize: selectedStepSize, + unselectedStepSize: unselectedStepSize, + startingAngle: startingAngleTopOfIndicator, + roundedCap: roundedCap, + gradientColor: gradientColor, + textDirection: textDirection, + removeRoundedCapExtraAngle: removeRoundedCapExtraAngle, + ), + // Padding needed to show the indicator when child is placed on top of it + child: Padding( + padding: EdgeInsets.all(maxDefinedSize), + child: child, + ), + ), + ), + ); + } + + /// Compute the maximum possible size of the indicator between + /// [stepSize] and [customStepSize] + double get maxDefinedSize { + if (customStepSize == null) { + return math.max(stepSize, math.max(selectedStepSize ?? 0, unselectedStepSize ?? 0)); + } + + // When customSize defined, compute and return max possible size + double currentMaxSize = 0; + + for (int step = 0; step < totalSteps; ++step) { + // Consider max between selected and unselected case + final customSizeValue = math.max(customStepSize!(step, false), customStepSize!(step, true)); + if (customSizeValue > currentMaxSize) { + currentMaxSize = customSizeValue; + } + } + + return currentMaxSize; + } + + /// Make [startingAngle] to top-center of indicator (0°) by default + double get startingAngleTopOfIndicator => startingAngle - math.pi / 2; +} + +class _CircularIndicatorPainter implements CustomPainter { + final int totalSteps; + final int currentStep; + final double padding; + final Color? selectedColor; + final Color? unselectedColor; + final double stepSize; + final double? selectedStepSize; + final double? unselectedStepSize; + final double Function(int, bool)? customStepSize; + final double maxDefinedSize; + final Color Function(int)? customColor; + final CircularDirection circularDirection; + final double startingAngle; + final double arcSize; + final bool Function(int, bool)? roundedCap; + final Gradient? gradientColor; + final TextDirection textDirection; + final bool removeRoundedCapExtraAngle; + + _CircularIndicatorPainter({ + required this.totalSteps, + required this.circularDirection, + required this.customColor, + required this.currentStep, + required this.selectedColor, + required this.unselectedColor, + required this.padding, + required this.stepSize, + required this.selectedStepSize, + required this.unselectedStepSize, + required this.customStepSize, + required this.startingAngle, + required this.arcSize, + required this.maxDefinedSize, + required this.roundedCap, + required this.gradientColor, + required this.textDirection, + required this.removeRoundedCapExtraAngle, + }); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width; + final h = size.height; + + // Step length is user-defined arcSize + // divided by the total number of steps (each step same size) + final stepLength = arcSize / totalSteps; + + // Define general arc paint + Paint paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = maxDefinedSize; + + final rect = Rect.fromCenter( + // Rect created from the center of the widget + center: Offset(w / 2, h / 2), + // For both height and width, subtract maxDefinedSize to fit indicator inside the parent container + height: h - maxDefinedSize, + width: w - maxDefinedSize, + ); + + if (gradientColor != null) { + paint.shader = gradientColor!.createShader(rect, textDirection: textDirection); + } + + // Change color selected or unselected based on the circularDirection + final isClockwise = circularDirection == CircularDirection.clockwise; + + // Make a continuous arc without rendering all the steps when possible + if (padding == 0 && customColor == null && customStepSize == null && roundedCap == null) { + _drawContinuousArc(canvas, paint, rect, isClockwise); + } else { + _drawStepArc(canvas, paint, rect, isClockwise, stepLength); + } + } + + /// Draw a series of arcs, each composing the full steps of the indicator + void _drawStepArc(Canvas canvas, Paint paint, Rect rect, bool isClockwise, double stepLength) { + // Draw a series of circular arcs to compose the indicator + // Starting based on startingAngle attribute + // + // When clockwise: + // - Start drawing counterclockwise so to have the selected steps on top of the unselected + int step = isClockwise ? totalSteps - 1 : 0; + double stepAngle = isClockwise ? startingAngle - stepLength : startingAngle; + for (; isClockwise ? step >= 0 : step < totalSteps; isClockwise ? stepAngle -= stepLength : stepAngle += stepLength, isClockwise ? --step : ++step) { + // Check if the current step is selected or unselected + final isSelectedColor = _isSelectedColor(step, isClockwise); + + // Size of the step + final indexStepSize = customStepSize != null + // Consider step index inverted when counterclockwise + ? customStepSize!(_indexOfStep(step, isClockwise), isSelectedColor) + : isSelectedColor + ? selectedStepSize ?? stepSize + : unselectedStepSize ?? stepSize; + + // Use customColor if defined + final stepColor = customColor != null + // Consider step index inverted when counterclockwise + ? customColor!(_indexOfStep(step, isClockwise)) + : isSelectedColor + ? selectedColor! + : unselectedColor!; + + // Apply stroke cap to each step + final hasStrokeCap = roundedCap != null ? roundedCap!(_indexOfStep(step, isClockwise), isSelectedColor) : false; + final strokeCap = hasStrokeCap ? StrokeCap.round : StrokeCap.butt; + + // Remove extra size caused by rounded stroke cap + // https://github.com/SandroMaglione/step-progress-indicator/issues/20#issue-786114745 + final extraCapSize = indexStepSize / 2; + final extraCapAngle = extraCapSize / (rect.width / 2); + final extraCapRemove = hasStrokeCap && removeRoundedCapExtraAngle; + + // Draw arc steps of the indicator + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + startingAngle: stepAngle + (extraCapRemove ? extraCapAngle : 0), + sweepAngle: stepLength - padding - (extraCapRemove ? extraCapAngle * 2 : 0), + paint: paint, + color: stepColor, + strokeWidth: indexStepSize, + strokeCap: strokeCap, + ); + } + } + + /// Draw optimized continuous indicator instead of multiple steps + void _drawContinuousArc(Canvas canvas, Paint paint, Rect rect, bool isClockwise) { + // Compute color of the selected and unselected bars + final firstStepColor = isClockwise ? selectedColor : unselectedColor; + final secondStepColor = !isClockwise ? selectedColor : unselectedColor; + + // Selected and unselected step sizes if defined, otherwise use stepSize + final firstStepSize = isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize; + final secondStepSize = !isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize; + + // Compute length and starting angle of the selected and unselected bars + final firstArcLength = arcSize * (currentStep / totalSteps); + final secondArcLength = arcSize - firstArcLength; + + // firstArcStartingAngle = startingAngle + final secondArcStartingAngle = startingAngle + firstArcLength; + + // Apply stroke cap to both arcs + // NOTE: For continuous circular indicator, it uses 0 and 1 as index to + // apply the rounded cap + final firstArcStrokeCap = roundedCap != null + ? isClockwise + ? roundedCap!(0, true) + : roundedCap!(1, false) + : false; + final secondArcStrokeCap = roundedCap != null + ? isClockwise + ? roundedCap!(1, false) + : roundedCap!(0, true) + : false; + final firstCap = firstArcStrokeCap ? StrokeCap.round : StrokeCap.butt; + final secondCap = secondArcStrokeCap ? StrokeCap.round : StrokeCap.butt; + + // When clockwise, draw the second arc first and the first on top of it + // Required when stroke cap is rounded to make the selected step on top of the unselected + if (circularDirection == CircularDirection.clockwise) { + // Second arc, selected when counterclockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: secondArcStartingAngle, + sweepAngle: secondArcLength, + strokeWidth: secondStepSize, + color: secondStepColor!, + strokeCap: secondCap, + ); + + // First arc, selected when clockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: startingAngle, + sweepAngle: firstArcLength, + strokeWidth: firstStepSize, + color: firstStepColor!, + strokeCap: firstCap, + ); + } else { + // First arc, selected when clockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: startingAngle, + sweepAngle: firstArcLength, + strokeWidth: firstStepSize, + color: firstStepColor!, + strokeCap: firstCap, + ); + + // Second arc, selected when counterclockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: secondArcStartingAngle, + sweepAngle: secondArcLength, + strokeWidth: secondStepSize, + color: secondStepColor!, + strokeCap: secondCap, + ); + } + } + + /// Draw the actual arc for a continuous indicator + void _drawArcOnCanvas({ + required Canvas canvas, + required Rect rect, + required double startingAngle, + required double sweepAngle, + required Paint paint, + required Color color, + required double strokeWidth, + required StrokeCap strokeCap, + }) => + canvas.drawArc( + rect, + startingAngle, + sweepAngle, + false /*isRadial*/, + paint + ..color = color + ..strokeWidth = strokeWidth + ..strokeCap = strokeCap, + ); + + bool _isSelectedColor(int step, bool isClockwise) => isClockwise ? step < currentStep : (step + 1) > (totalSteps - currentStep); + + /// Start counting indexes from the right if clockwise and on the left if counterclockwise + int _indexOfStep(int step, bool isClockwise) => isClockwise ? step : totalSteps - step - 1; + + @override + bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this; + + @override + bool hitTest(Offset position) => false; + + @override + void addListener(listener) {} + + @override + void removeListener(listener) {} + + @override + get semanticsBuilder => null; + + @override + bool shouldRebuildSemantics(CustomPainter oldDelegate) => false; +} + +/// Used to define the [circularDirection] attribute of the [CircularStepProgressBar] +enum CircularDirection { + clockwise, + counterclockwise, +}