login agnimation has been fixed + request holiday screen was created

This commit is contained in:
Daniah Ayad Al-sultani
2025-12-04 17:23:06 +03:00
parent 209080842a
commit 08132b52a9
11 changed files with 1200 additions and 443 deletions

View File

@@ -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,
),
),
);

View File

@@ -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<HolidayScreen> createState() => _HolidayScreenState();
}
class _HolidayScreenState extends State<HolidayScreen> {
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),

View File

@@ -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<RequestLeaveScreen> createState() => _RequestLeaveScreenState();
}
class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
// 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<void> 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<void> 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<String>(
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 "";
}
}
}

View File

@@ -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<LoginAnimationScreen> createState() => _LoginAnimationScreenState();
}
class _LoginAnimationScreenState extends State<LoginAnimationScreen>
with TickerProviderStateMixin {
late AnimationController ringController;
late AnimationController progressController;
late Animation<double> ringAnimation;
late Animation<double> progressAnimation;
// ERROR pulse animation
late AnimationController errorPulseController;
late Animation<double> errorPulseAnimation;
// SUCCESS one-time pulse
late AnimationController successPulseController;
late Animation<double> 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<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(parent: ringController, curve: Curves.easeOut));
progressAnimation = Tween<double>(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<double>(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<double>(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;
}

View File

@@ -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<LoginAnimationScreen> createState() => _LoginAnimationScreenState();
}
class _LoginAnimationScreenState extends State<LoginAnimationScreen>
with TickerProviderStateMixin {
late AnimationController ringController;
late AnimationController progressController;
late Animation<double> ringAnimation;
late Animation<double> 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<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: ringController, curve: Curves.easeOut));
progressAnimation = Tween<double>(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: <Widget>[
...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;
}

View File

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