OHO-25(Updated UI)

This commit is contained in:
lokeshramchand-ctrl 2025-08-28 14:04:45 +05:30
parent d1964c0a10
commit 394da30b82
13 changed files with 1821 additions and 934 deletions

BIN
Frontend/assets/money.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -1,126 +1,13 @@
// // ignore_for_file: use_build_context_synchronously
// import 'package:flutter/material.dart';
// import 'package:google_sign_in/google_sign_in.dart';
// import 'package:http/http.dart' as http;
// import 'package:monarch/email.dart';
// import 'package:monarch/other_pages/enviroment.dart';
// import 'dart:convert';
// import 'package:shared_preferences/shared_preferences.dart';
// import 'package:monarch/main_pages/HomePage/homepage.dart';
// class LoginScreen extends StatefulWidget {
// const LoginScreen({super.key});
// @override
// State<LoginScreen> createState() => _LoginScreenState();
// }
// class _LoginScreenState extends State<LoginScreen> {
// // GoogleSignIn instance
// final GoogleSignIn _googleSignIn = GoogleSignIn(
// scopes: ['email', 'profile', 'openid'],
// serverClientId: '${Environment.serverClientId}',
// );
// bool _isLoading = false;
// Future<void> _handleGoogleSignIn() async {
// setState(() => _isLoading = true);
// try {
// // Step 1: Trigger the sign-in flow
// final GoogleSignInAccount? account = await _googleSignIn.signIn();
// if (account == null) {
// // User cancelled
// setState(() => _isLoading = false);
// return;
// }
// // Step 2: Get authentication tokens
// final GoogleSignInAuthentication auth = await account.authentication;
// final String? idToken = auth.idToken;
// if (idToken == null) {
// debugPrint("❌ Failed to get ID token");
// return;
// }
// // Step 3: Send token to backend
// final uri = Uri.parse("${Environment.baseUrl}/auth/google/token");
// final res = await http.post(
// uri,
// headers: {"Content-Type": "application/json"},
// body: jsonEncode({"idToken": idToken}),
// );
// if (res.statusCode == 200) {
// final data = jsonDecode(res.body);
// if (data['success'] == true) {
// debugPrint("✅ User logged in: ${data['user']}");
// final prefs = await SharedPreferences.getInstance();
// prefs.setString('userId', data['user']['_id']);
// Navigator.pushReplacement(
// context,
// MaterialPageRoute(builder: (_) => const EmailsScreen(accessToken: '', userId: 'userId',)),
// );
// } else {
// debugPrint("❌ Login failed: ${data['error']}");
// }
// } else {
// debugPrint("❌ Backend error: ${res.body}");
// }
// } catch (e) {
// debugPrint("⚠ Google Sign-In error: $e");
// } finally {
// setState(() => _isLoading = false);
// }
// }
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// backgroundColor: Colors.white,
// body: Center(
// child:
// _isLoading
// ? const CircularProgressIndicator()
// : ElevatedButton.icon(
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.white,
// foregroundColor: Colors.black,
// padding: const EdgeInsets.symmetric(
// vertical: 12,
// horizontal: 20,
// ),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(30),
// side: const BorderSide(color: Colors.grey),
// ),
// ),
// label: const Text(
// 'Sign in with Google',
// style: TextStyle(fontSize: 16),
// ),
// onPressed: _handleGoogleSignIn,
// ),
// ),
// );
// }
// }
// ignore_for_file: use_build_context_synchronously, unused_import
// ignore_for_file: use_build_context_synchronously, deprecated_member_use, unused_import
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
import 'package:monarch/main_pages/HomePage/homepage.dart';
import 'package:monarch/main_pages/HomePage/Components/Voice/voice_dialog.dart';
//import 'package:monarch/email.dart';
import 'package:monarch/other_pages/enviroment.dart';
import 'dart:convert';
import 'dart:math' as math;
import 'package:shared_preferences/shared_preferences.dart';
class LoginScreen extends StatefulWidget {
@ -130,7 +17,8 @@ class LoginScreen extends StatefulWidget {
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
class _LoginScreenState extends State<LoginScreen>
with TickerProviderStateMixin {
final GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: [
'email',
@ -142,6 +30,88 @@ class _LoginScreenState extends State<LoginScreen> {
);
bool _isLoading = false;
late AnimationController _fadeController;
late AnimationController _slideController;
late AnimationController _scaleController;
late AnimationController _rotationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_initAnimations();
_startAnimations();
}
void _initAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_rotationController = AnimationController(
duration: const Duration(seconds: 20),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _slideController, curve: Curves.elasticOut),
);
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut),
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * math.pi,
).animate(_rotationController);
}
void _startAnimations() {
Future.delayed(const Duration(milliseconds: 300), () {
_fadeController.forward();
});
Future.delayed(const Duration(milliseconds: 500), () {
_slideController.forward();
});
Future.delayed(const Duration(milliseconds: 700), () {
_scaleController.forward();
});
_rotationController.repeat();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
_scaleController.dispose();
_rotationController.dispose();
super.dispose();
}
Future<void> _handleGoogleSignIn() async {
setState(() => _isLoading = true);
@ -164,7 +134,6 @@ class _LoginScreenState extends State<LoginScreen> {
return;
}
// Send idToken to backend for login
final uri = Uri.parse("${Environment.baseUrl}/auth/google/token");
final res = await http.post(
uri,
@ -179,18 +148,9 @@ class _LoginScreenState extends State<LoginScreen> {
prefs.setString('userId', data['user']['_id']);
prefs.setString('accessToken', accessToken);
// Navigate directly to EmailsScreen with accessToken
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => FinTrackHomePage(),
// builder:
// (_) => EmailsScreen(
// accessToken: accessToken,
// userId: data['user']['_id'],
// ),
//builder: (_) => AutoSyncEmailsScreen(accessToken: accessToken,userId: data['user']['_id'],),
),
MaterialPageRoute(builder: (_) => FinTrackHomePage()),
);
} else {
debugPrint("❌ Login failed: ${data['error']}");
@ -207,32 +167,347 @@ class _LoginScreenState extends State<LoginScreen> {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: Center(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF1DB584), // Your app's primary green
Color(0xFF0FA968),
Color(0xFF059649),
],
stops: [0.0, 0.5, 1.0],
),
),
child: Stack(
children: [
// Animated Background Elements
...List.generate(6, (index) {
return AnimatedBuilder(
animation: _rotationController,
builder: (context, child) {
return Positioned(
top: size.height * (0.1 + (index * 0.15)),
left: size.width * (0.1 + (index * 0.12)),
child: Transform.rotate(
angle: _rotationAnimation.value + (index * 0.5),
child: Container(
width: 60 + (index * 10),
height: 60 + (index * 10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.05),
border: Border.all(
color: Colors.white.withOpacity(0.1),
width: 2,
),
),
),
),
);
},
);
}),
// Floating Particles
...List.generate(12, (index) {
return AnimatedBuilder(
animation: _rotationController,
builder: (context, child) {
return Positioned(
top: size.height * (0.2 + (index * 0.06)),
right: size.width * (0.1 + (index * 0.05)),
child: Transform.translate(
offset: Offset(
math.sin(_rotationAnimation.value + index) * 20,
math.cos(_rotationAnimation.value + index) * 15,
),
child: Container(
width: 8 + (index % 3) * 4,
height: 8 + (index % 3) * 4,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.3),
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.1),
blurRadius: 10,
spreadRadius: 2,
),
],
),
),
),
);
},
);
}),
// Main Content
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
const Spacer(),
// Logo Section
FadeTransition(
opacity: _fadeAnimation,
child: Column(
children: [
ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 30,
spreadRadius: 5,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.account_balance_wallet_rounded,
size: 60,
color: Color(0xFF1DB584),
),
),
),
const SizedBox(height: 32),
// App Title
const Text(
'FinTrack',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 42,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: -1.5,
),
),
const SizedBox(height: 12),
// Subtitle
Text(
'Your Smart Financial Companion',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w400,
letterSpacing: 0.3,
),
),
],
),
),
const Spacer(),
// Features Section
SlideTransition(
position: _slideAnimation,
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Column(
children: [
Row(
children: [
_buildFeatureItem(
Icons.trending_up_rounded,
'Track Expenses',
),
const SizedBox(width: 24),
_buildFeatureItem(
Icons.insights_rounded,
'Smart Analytics',
),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildFeatureItem(
Icons.security_rounded,
'Secure & Private',
),
const SizedBox(width: 24),
_buildFeatureItem(
Icons.sync_rounded,
'Auto Sync',
),
],
),
],
),
),
),
const SizedBox(height: 48),
// Google Sign In Button
SlideTransition(
position: _slideAnimation,
child:
_isLoading
? const CircularProgressIndicator()
: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 20,
),
shape: RoundedRectangleBorder(
? Container(
width: double.infinity,
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
side: const BorderSide(color: Colors.grey),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 20,
spreadRadius: 2,
offset: const Offset(0, 8),
),
],
),
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF1DB584),
),
),
label: const Text(
'Sign in with Google',
style: TextStyle(fontSize: 16),
),
onPressed: _handleGoogleSignIn,
),
)
: Container(
width: double.infinity,
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 20,
spreadRadius: 2,
offset: const Offset(0, 8),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _handleGoogleSignIn,
borderRadius: BorderRadius.circular(30),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Image.network(
'https://developers.google.com/identity/images/g-logo.png',
width: 24,
height: 24,
),
const SizedBox(width: 16),
const Text(
'Continue with Google',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
letterSpacing: 0.2,
),
),
],
),
),
),
),
),
),
const SizedBox(height: 24),
// Terms Text
FadeTransition(
opacity: _fadeAnimation,
child: Text(
'By continuing, you agree to our Terms of Service\nand Privacy Policy',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: Colors.white.withOpacity(0.7),
fontWeight: FontWeight.w400,
height: 1.5,
),
),
),
const SizedBox(height: 48),
],
),
),
),
],
),
),
);
}
Widget _buildFeatureItem(IconData icon, String text) {
return Expanded(
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: Colors.white, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: const TextStyle(
fontFamily: 'Inter',
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}

View file

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:monarch/login.dart';
import 'package:monarch/speech.dart';
import 'package:monarch/testing_pages/speech.dart';
import 'package:monarch/support/add.dart';
import 'package:monarch/main_pages/Statistics/Budget/update_budget.dart';
import 'package:monarch/main_pages/HomePage/homepage.dart';

View file

@ -1,15 +1,31 @@
// ignore_for_file: deprecated_member_use, unused_import
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:monarch/main_pages/HomePage/homepage.dart';
import 'dart:convert';
import 'dart:math' as math;
import 'package:monarch/other_pages/colors.dart';
import 'package:monarch/other_pages/enviroment.dart';
import 'package:shared_preferences/shared_preferences.dart';
extension ResponsiveExtension on BuildContext {
double responsiveWidth(double size) {
return size * (MediaQuery.of(this).size.width / 375);
}
double responsiveHeight(double size) {
return size * (MediaQuery.of(this).size.height / 812);
}
double responsiveText(double size) {
final width = MediaQuery.of(this).size.width;
if (width < 350) return size * 0.9;
if (width > 600) return size * 1.2;
return size;
}
}
class EmailTransactionDialog extends StatefulWidget {
const EmailTransactionDialog({super.key});
@ -17,7 +33,8 @@ class EmailTransactionDialog extends StatefulWidget {
State<EmailTransactionDialog> createState() => _EmailTransactionDialogState();
}
class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
class _EmailTransactionDialogState extends State<EmailTransactionDialog>
with TickerProviderStateMixin {
final List<dynamic> _emails = [];
bool _loading = true;
String? _status;
@ -25,13 +42,98 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
String? _error;
String? _userId;
String? _accessToken;
@override
late AnimationController _slideController;
late AnimationController _fadeController;
late AnimationController _scaleController;
late AnimationController _rotationController;
late AnimationController _progressController;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
late Animation<double> _progressAnimation;
@override
void initState() {
super.initState();
_initAnimations();
_startEntryAnimation();
_autoSyncEmails();
}
void _initAnimations() {
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_rotationController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_progressController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _slideController, curve: Curves.elasticOut),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
);
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut),
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * math.pi,
).animate(_rotationController);
_progressAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _progressController, curve: Curves.easeInOut),
);
}
void _startEntryAnimation() {
_slideController.forward();
Future.delayed(const Duration(milliseconds: 200), () {
_fadeController.forward();
});
Future.delayed(const Duration(milliseconds: 400), () {
_scaleController.forward();
});
}
@override
void dispose() {
_slideController.dispose();
_fadeController.dispose();
_scaleController.dispose();
_rotationController.dispose();
_progressController.dispose();
super.dispose();
}
Future<void> _autoSyncEmails() async {
setState(() {
_loading = true;
@ -39,6 +141,9 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
_syncedCount = 0;
});
_rotationController.repeat();
_progressController.forward();
try {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('userId');
@ -52,7 +157,6 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success'] == true && data['count'] != null) {
setState(() {
_loading = false;
@ -60,216 +164,442 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
_status = "✅ Synced $_syncedCount transactions successfully!";
});
// // Optional: navigate back or to another screen after a delay
// Future.delayed(const Duration(seconds: 2), () {
// Navigator.pushReplacement(
// context,
// // MaterialPageRoute(builder: (_) => const FinTrackHomePage()),
// );
// });
_rotationController.stop();
_progressController.reset();
// Success animation
_scaleController.reset();
_scaleController.forward();
} else {
setState(() {
_loading = false;
_status = "❌ Sync failed: Unexpected response format";
});
_rotationController.stop();
}
} else {
setState(() {
_loading = false;
_status = "❌ Backend error: ${response.body}";
});
_rotationController.stop();
}
} catch (error) {
setState(() {
_loading = false;
_status = "⚠️ Network or unexpected error: $error";
});
_rotationController.stop();
}
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.7,
return SlideTransition(
position: _slideAnimation,
child: Container(
height: context.responsiveHeight(560),
decoration: BoxDecoration(
color: cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
borderRadius: BorderRadius.vertical(
top: Radius.circular(context.responsiveWidth(28)),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withOpacity(0.08),
blurRadius: 32,
offset: const Offset(0, -8),
spreadRadius: 2,
),
BoxShadow(
color: primaryColor.withOpacity(0.05),
blurRadius: 16,
offset: const Offset(0, -4),
),
],
),
child: Column(
children: [
// Top drag handle
Container(
width: 48,
height: 4,
margin: const EdgeInsets.only(top: 16, bottom: 24),
// Drag Handle with Animation
FadeTransition(
opacity: _fadeAnimation,
child: Container(
width: context.responsiveWidth(48),
height: context.responsiveHeight(4),
margin: EdgeInsets.only(
top: context.responsiveHeight(16),
bottom: context.responsiveHeight(24),
),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(2),
borderRadius: BorderRadius.circular(
context.responsiveWidth(2),
),
),
),
),
// Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
// Animated Header
FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: context.responsiveWidth(24),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
// Animated Icon Container
ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: context.responsiveWidth(56),
height: context.responsiveWidth(56),
decoration: BoxDecoration(
color: accentColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
accentColor.withOpacity(0.1),
accentColor.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
border: Border.all(
color: accentColor.withOpacity(0.2),
width: 1,
width: 1.5,
),
boxShadow: [
BoxShadow(
color: accentColor.withOpacity(0.1),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Icon(
Icons.email_rounded,
color: accentColor,
size: context.responsiveWidth(28),
),
),
child: Icon(Icons.mail_outline, color: accentColor, size: 24),
),
const SizedBox(width: 16),
Column(
SizedBox(width: context.responsiveWidth(16)),
// Title Section
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Email Transactions',
style: GoogleFonts.inter(
color: primaryColor,
fontSize: 20,
fontSize: context.responsiveText(20),
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
),
),
SizedBox(height: context.responsiveHeight(2)),
Text(
'Forward receipts to auto-add transactions',
style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6),
fontSize: 14,
fontSize: context.responsiveText(13),
fontWeight: FontWeight.w500,
height: 1.2,
),
),
],
),
const Spacer(),
IconButton(
icon: Icon(Icons.refresh, color: accentColor),
onPressed: _autoSyncEmails,
),
],
),
),
const SizedBox(height: 16),
// Body (loading / error / empty / list)
Expanded(
child:
_loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
_error!,
textAlign: TextAlign.center,
style: GoogleFonts.inter(
color: Colors.redAccent,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
)
: _emails.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.inbox_outlined,
size: 48,
color: primaryColor.withOpacity(0.4),
),
const SizedBox(height: 12),
Text(
"No transactions found",
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
const SizedBox(height: 4),
Text(
"Forward your receipts to see them here.",
style: GoogleFonts.inter(
fontSize: 13,
color: primaryColor.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
],
),
)
: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
itemCount: _emails.length,
separatorBuilder:
(_, __) => Divider(
height: 1,
color: primaryColor.withOpacity(0.1),
),
itemBuilder: (context, index) {
final email = _emails[index];
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Container(
height: 40,
width: 40,
// Animated Refresh Button
ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: context.responsiveWidth(44),
height: context.responsiveWidth(44),
decoration: BoxDecoration(
color: accentColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(
context.responsiveWidth(12),
),
border: Border.all(
color: accentColor.withOpacity(0.2),
width: 1,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _autoSyncEmails,
borderRadius: BorderRadius.circular(
context.responsiveWidth(12),
),
child: AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle:
_loading ? _rotationAnimation.value : 0,
child: Icon(
Icons.receipt_long,
Icons.refresh_rounded,
color: accentColor,
size: 22,
),
),
title: Text(
email['subject'] ?? "No Subject",
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: primaryColor,
),
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
email['snippet'] ?? "No details available",
style: GoogleFonts.inter(
fontSize: 13,
color: primaryColor.withOpacity(0.6),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Icon(
Icons.chevron_right,
color: primaryColor.withOpacity(0.4),
size: context.responsiveWidth(22),
),
);
},
),
),
),
),
),
],
),
),
),
SizedBox(height: context.responsiveHeight(32)),
// Main Content Area
Expanded(
child: _loading ? _buildLoadingContent() : _buildResultContent(),
),
],
),
),
);
}
Widget _buildLoadingContent() {
return FadeTransition(
opacity: _fadeAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Animated Loading Circle
ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: context.responsiveWidth(80),
height: context.responsiveWidth(80),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
accentColor.withOpacity(0.1),
accentColor.withOpacity(0.05),
],
),
border: Border.all(
color: accentColor.withOpacity(0.2),
width: 2,
),
),
child: AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: Icon(
Icons.sync_rounded,
color: accentColor,
size: context.responsiveWidth(36),
),
);
},
),
),
),
SizedBox(height: context.responsiveHeight(32)),
// Progress Bar
Container(
width: context.responsiveWidth(200),
height: context.responsiveHeight(6),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(context.responsiveWidth(3)),
),
child: AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: _progressAnimation.value,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [accentColor, accentColor.withOpacity(0.7)],
),
borderRadius: BorderRadius.circular(
context.responsiveWidth(3),
),
),
),
);
},
),
),
SizedBox(height: context.responsiveHeight(24)),
// Status Text
Text(
_status ?? "Processing...",
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: context.responsiveText(16),
fontWeight: FontWeight.w600,
color: primaryColor.withOpacity(0.8),
height: 1.4,
),
),
],
),
);
}
Widget _buildResultContent() {
final isSuccess = _status?.contains('✅') == true;
final isError =
_status?.contains('❌') == true || _status?.contains('⚠️') == true;
return FadeTransition(
opacity: _fadeAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Result Icon
ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: context.responsiveWidth(80),
height: context.responsiveWidth(80),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors:
isSuccess
? [
Colors.green.withOpacity(0.1),
Colors.green.withOpacity(0.05),
]
: isError
? [
Colors.red.withOpacity(0.1),
Colors.red.withOpacity(0.05),
]
: [
accentColor.withOpacity(0.1),
accentColor.withOpacity(0.05),
],
),
border: Border.all(
color:
isSuccess
? Colors.green.withOpacity(0.3)
: isError
? Colors.red.withOpacity(0.3)
: accentColor.withOpacity(0.3),
width: 2,
),
),
child: Icon(
isSuccess
? Icons.check_circle_rounded
: isError
? Icons.error_rounded
: Icons.info_rounded,
color:
isSuccess
? Colors.green
: isError
? Colors.red
: accentColor,
size: context.responsiveWidth(36),
),
),
),
SizedBox(height: context.responsiveHeight(24)),
// Status Text
Padding(
padding: EdgeInsets.symmetric(
horizontal: context.responsiveWidth(32),
),
child: Text(
_status ?? "Done",
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: context.responsiveText(16),
fontWeight: FontWeight.w600,
color: primaryColor.withOpacity(0.9),
height: 1.4,
),
),
),
SizedBox(height: context.responsiveHeight(32)),
// Action Button
ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: context.responsiveWidth(180),
height: context.responsiveHeight(48),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [accentColor, accentColor.withOpacity(0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(
context.responsiveWidth(24),
),
boxShadow: [
BoxShadow(
color: accentColor.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _autoSyncEmails,
borderRadius: BorderRadius.circular(
context.responsiveWidth(24),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.refresh_rounded,
color: Colors.white,
size: context.responsiveWidth(20),
),
SizedBox(width: context.responsiveWidth(8)),
Text(
"Retry Sync",
style: GoogleFonts.inter(
color: Colors.white,
fontSize: context.responsiveText(14),
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
),
],
),
),
),
),
),
],
),
);

View file

@ -1,10 +1,14 @@
// ignore_for_file: deprecated_member_use
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:monarch/support/add.dart';
import 'package:monarch/main_pages/HomePage/homepage.dart';
import 'package:monarch/main_pages/Statistics/statistics.dart';
class CustomNavBar extends StatelessWidget {
final int currentIndex;
final Function(int) onTap;
final void Function(int) onTap;
final Color backgroundColor;
final Color accentColor;
final Color primaryColor;
@ -18,88 +22,103 @@ class CustomNavBar extends StatelessWidget {
required this.accentColor,
required this.primaryColor,
required this.cardColor,
required FloatingActionButtonLocation floatingActionButtonLocation,
});
// Navigation items
final List _navItems = const [
NavItem(icon: Icons.home_rounded, index: 0, page: FinTrackHomePage()),
NavItem(icon: Icons.bar_chart_rounded, index: 1, page: Statistics()),
NavItem(icon: Icons.settings_rounded, index: 2, page: AddExpenseScreen()),
];
@override
Widget build(BuildContext context) {
return Container(
height: 80, // Fixed height for bottom navigation
return Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.symmetric(horizontal: 20),
height: 72,
width: MediaQuery.of(context).size.width * 0.7, // centered & responsive
decoration: BoxDecoration(
color: backgroundColor,
color: cardColor.withOpacity(0.65),
borderRadius: BorderRadius.circular(40),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
color: primaryColor.withOpacity(0.15),
blurRadius: 30,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(40),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavItem(
icon: Icons.home_outlined,
activeIcon: Icons.home_rounded,
label: 'Home',
index: 0,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children:
_navItems
.map((item) => _buildNavItem(context, item))
.toList(),
),
_buildNavItem(
icon: Icons.analytics_outlined,
activeIcon: Icons.analytics_rounded,
label: 'Analytics',
index: 1,
),
_buildNavItem(
icon: Icons.account_balance_wallet_outlined,
activeIcon: Icons.account_balance_wallet_rounded,
label: 'Budget',
index: 2,
),
_buildNavItem(
icon: Icons.person_outlined,
activeIcon: Icons.person_rounded,
label: 'Profile',
index: 3,
),
],
),
);
}
Widget _buildNavItem({
required IconData icon,
required IconData activeIcon,
required String label,
required int index,
}) {
final isActive = currentIndex == index;
return Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => onTap(index),
Widget _buildNavItem(BuildContext context, NavItem item) {
final isActive = currentIndex == item.index;
return GestureDetector(
onTap: () {
onTap(item.index);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => item.page),
);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isActive ? activeIcon : icon,
color: isActive ? accentColor : primaryColor.withOpacity(0.6),
AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: isActive ? 38 : 30,
height: isActive ? 38 : 30,
decoration: BoxDecoration(
color:
isActive
? primaryColor.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
child: Icon(
item.icon,
color: isActive ? primaryColor : primaryColor.withOpacity(0.4),
size: 24,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isActive ? accentColor : primaryColor.withOpacity(0.6),
fontSize: 12,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 6,
height: 6,
decoration: BoxDecoration(
color: isActive ? accentColor : Colors.transparent,
shape: BoxShape.circle,
),
),
],
),
),
),
);
}
}
// Nav item model
class NavItem {
final IconData icon;
final int index;
final Widget page;
const NavItem({required this.icon, required this.index, required this.page});
}

View file

@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:monarch/main_pages/Statistics/statistics.dart';
import 'package:monarch/support/add.dart';
import 'package:monarch/other_pages/colors.dart';
@ -66,8 +67,13 @@ class QuickActionsPage extends StatelessWidget {
Expanded(
child: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
// Navigate to Statistics
HapticFeedback.heavyImpact();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const Statistics(),
),
);
},
child: Container(
padding: const EdgeInsets.all(20),

View file

@ -9,14 +9,50 @@ import 'package:monarch/other_pages/colors.dart';
import 'package:monarch/other_pages/enviroment.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ConfirmTransactionPage extends StatelessWidget {
class ConfirmTransactionPage extends StatefulWidget {
final Map<String, dynamic> data;
const ConfirmTransactionPage({super.key, required this.data});
@override
State<ConfirmTransactionPage> createState() => _ConfirmTransactionPageState();
}
class _ConfirmTransactionPageState extends State<ConfirmTransactionPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.2),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _saveTransaction(BuildContext context) async {
const String saveUrl =
'${Environment.baseUrl}/api/transaction/add'; // ✅ make sure this matches backend
const String saveUrl = '${Environment.baseUrl}/api/transaction/add';
try {
final prefs = await SharedPreferences.getInstance();
@ -31,12 +67,10 @@ class ConfirmTransactionPage extends StatelessWidget {
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'userId': userId,
'description': data['description'] ?? '',
'amount':
double.tryParse(data['amount'].toString()) ??
0.0, // ✅ force number
'category': data['category'] ?? 'Uncategorized',
'date': data['date'] ?? DateTime.now().toIso8601String(),
'description': widget.data['description'] ?? '',
'amount': double.tryParse(widget.data['amount'].toString()) ?? 0.0,
'category': widget.data['category'] ?? 'Uncategorized',
'date': widget.data['date'] ?? DateTime.now().toIso8601String(),
}),
);
@ -47,11 +81,9 @@ class ConfirmTransactionPage extends StatelessWidget {
icon: Icons.check_circle,
backgroundColor: const Color(0xFF4CAF50),
iconColor: Colors.white,
duration: const Duration(seconds: 3),
);
Navigator.popUntil(context, (route) => route.isFirst);
} else {
// 👇 Log backend response
throw Exception('Save failed: ${response.statusCode} ${response.body}');
}
} catch (e) {
@ -61,7 +93,6 @@ class ConfirmTransactionPage extends StatelessWidget {
icon: Icons.error_outline,
backgroundColor: const Color(0xFFFF6B6B),
iconColor: Colors.white,
duration: const Duration(seconds: 4),
);
}
}
@ -76,47 +107,29 @@ class ConfirmTransactionPage extends StatelessWidget {
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Container(
padding: EdgeInsets.symmetric(vertical: context.responsiveHeight(4)),
child: Row(
content: Row(
children: [
Container(
padding: EdgeInsets.all(context.responsiveWidth(8)),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(
context.responsiveWidth(8),
),
),
child: Icon(
icon,
color: iconColor,
size: context.responsiveText(20),
),
),
SizedBox(width: context.responsiveWidth(12)),
Icon(icon, color: iconColor, size: context.responsiveText(20)),
SizedBox(width: context.responsiveWidth(8)),
Expanded(
child: Text(
message,
style: GoogleFonts.inter(
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: Colors.white,
fontSize: context.responsiveText(14),
height: 1.4,
),
),
),
],
),
),
backgroundColor: backgroundColor,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(context.responsiveWidth(12)),
duration: duration,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(context.responsiveWidth(12)),
),
margin: EdgeInsets.all(context.responsiveWidth(16)),
duration: duration,
elevation: 8,
),
);
}
@ -130,15 +143,19 @@ class ConfirmTransactionPage extends StatelessWidget {
return Row(
children: [
Container(
width: 40,
height: 40,
width: context.responsiveWidth(40),
height: context.responsiveWidth(40),
decoration: BoxDecoration(
color: accentColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
color: accentColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(context.responsiveWidth(10)),
),
child: Icon(icon, color: accentColor, size: 20),
child: Icon(
icon,
color: accentColor,
size: context.responsiveText(20),
),
const SizedBox(width: 16),
),
SizedBox(width: context.responsiveWidth(14)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -147,17 +164,16 @@ class ConfirmTransactionPage extends StatelessWidget {
label,
style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6),
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
fontSize: context.responsiveText(12),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
SizedBox(height: context.responsiveHeight(2)),
Text(
value,
style: GoogleFonts.inter(
color: isAmount ? Colors.green : primaryColor,
fontSize: 16,
fontSize: context.responsiveText(16),
fontWeight: FontWeight.w700,
),
),
@ -170,12 +186,12 @@ class ConfirmTransactionPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final amount = data['amount'];
final description = data['description'];
final category = data['category'];
final amount = widget.data['amount'];
final description = widget.data['description'];
final category = widget.data['category'];
final date =
data['date'] != null
? DateTime.parse(data['date']).toLocal()
widget.data['date'] != null
? DateTime.parse(widget.data['date']).toLocal()
: DateTime.now();
return Scaffold(
@ -184,11 +200,10 @@ class ConfirmTransactionPage extends StatelessWidget {
backgroundColor: Colors.transparent,
elevation: 0,
leading: Container(
margin: const EdgeInsets.all(8),
margin: EdgeInsets.all(context.responsiveWidth(6)),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: primaryColor.withOpacity(0.1), width: 1),
borderRadius: BorderRadius.circular(context.responsiveWidth(12)),
),
child: IconButton(
icon: Icon(Icons.arrow_back, color: primaryColor),
@ -199,81 +214,72 @@ class ConfirmTransactionPage extends StatelessWidget {
'Confirm Transaction',
style: GoogleFonts.inter(
color: primaryColor,
fontSize: 20,
fontSize: context.responsiveText(18),
fontWeight: FontWeight.w700,
),
),
centerTitle: true,
),
body: SafeArea(
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(24),
padding: EdgeInsets.all(context.responsiveWidth(20)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Card
// Header
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
padding: EdgeInsets.all(context.responsiveWidth(18)),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(
context.responsiveWidth(18),
),
boxShadow: [
BoxShadow(
color: accentColor.withOpacity(0.05),
blurRadius: 20,
offset: const Offset(0, 8),
color: accentColor.withOpacity(0.08),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
border: Border.all(
color: accentColor.withOpacity(0.1),
width: 1.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
child: Row(
children: [
Container(
width: 48,
height: 48,
padding: EdgeInsets.all(context.responsiveWidth(10)),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.green.withOpacity(0.2),
width: 1.5,
borderRadius: BorderRadius.circular(
context.responsiveWidth(10),
),
),
child: const Icon(
child: Icon(
Icons.check_circle_outline,
color: Colors.green,
size: 24,
size: context.responsiveText(22),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
SizedBox(width: context.responsiveWidth(14)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Review Transaction',
"Review Transaction",
style: GoogleFonts.inter(
color: primaryColor,
fontSize: 18,
fontSize: context.responsiveText(16),
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
Text(
'Please confirm the details below',
"Please confirm the details below",
style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6),
fontSize: 14,
fontSize: context.responsiveText(13),
fontWeight: FontWeight.w500,
),
),
],
color: primaryColor.withOpacity(0.6),
),
),
],
@ -282,17 +288,18 @@ class ConfirmTransactionPage extends StatelessWidget {
),
),
const SizedBox(height: 24),
SizedBox(height: context.responsiveHeight(24)),
// Transaction Details
// Details Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
padding: EdgeInsets.all(context.responsiveWidth(20)),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(
context.responsiveWidth(18),
),
border: Border.all(
color: primaryColor.withOpacity(0.1),
color: primaryColor.withOpacity(0.08),
width: 1,
),
),
@ -300,33 +307,27 @@ class ConfirmTransactionPage extends StatelessWidget {
children: [
_buildDetailItem(
icon: Icons.description_outlined,
label: 'Description',
value: description?.toString() ?? 'No description',
label: "Description",
value: description?.toString() ?? "No description",
),
const SizedBox(height: 20),
SizedBox(height: context.responsiveHeight(16)),
_buildDetailItem(
icon: Icons.payments_outlined,
label: 'Amount',
value: '₹$amount',
label: "Amount",
value: "₹$amount",
isAmount: true,
),
const SizedBox(height: 20),
SizedBox(height: context.responsiveHeight(16)),
_buildDetailItem(
icon: Icons.category_outlined,
label: 'Category',
value: category?.toString() ?? 'Uncategorized',
label: "Category",
value: category?.toString() ?? "Uncategorized",
),
const SizedBox(height: 20),
SizedBox(height: context.responsiveHeight(16)),
_buildDetailItem(
icon: Icons.calendar_today_outlined,
label: 'Date',
value: '${date.day}/${date.month}/${date.year}',
label: "Date",
value: "${date.day}/${date.month}/${date.year}",
),
],
),
@ -334,82 +335,77 @@ class ConfirmTransactionPage extends StatelessWidget {
const Spacer(),
// Action Buttons
// Buttons
Column(
children: [
// Confirm Button
Container(
// Confirm
GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) => _saveTransaction(context),
child: AnimatedScale(
scale: 1,
duration: const Duration(milliseconds: 150),
child: Container(
width: double.infinity,
height: 56,
padding: EdgeInsets.symmetric(
vertical: context.responsiveHeight(14),
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.green, Colors.green.shade600],
colors: [Colors.green, Colors.green.shade700],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(
context.responsiveWidth(14),
),
boxShadow: [
BoxShadow(
color: Colors.green.withOpacity(0.3),
blurRadius: 12,
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => _saveTransaction(context),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.check,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
'Confirm & Save Transaction',
style: GoogleFonts.inter(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
),
),
const SizedBox(height: 16),
// Cancel Button
Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: primaryColor.withOpacity(0.2),
width: 1,
),
),
child: TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Edit or Cancel',
"Confirm & Save Transaction",
style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.7),
fontSize: 16,
fontSize: context.responsiveText(15),
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
),
),
SizedBox(height: context.responsiveHeight(14)),
// Cancel
OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
minimumSize: Size(
double.infinity,
context.responsiveHeight(48),
),
side: BorderSide(
color: primaryColor.withOpacity(0.3),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
context.responsiveWidth(12),
),
),
),
child: Text(
"Edit or Cancel",
style: GoogleFonts.inter(
fontSize: context.responsiveText(14),
fontWeight: FontWeight.w600,
),
color: primaryColor.withOpacity(0.8),
),
),
),
@ -419,6 +415,8 @@ class ConfirmTransactionPage extends StatelessWidget {
),
),
),
),
),
);
}
}

View file

@ -1,15 +1,31 @@
// ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api, use_build_context_synchronously, deprecated_member_use
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:monarch/speech.dart';
import 'package:monarch/testing_pages/speech.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;
import 'package:monarch/other_pages/colors.dart';
import 'package:monarch/other_pages/enviroment.dart';
extension ResponsiveExtension on BuildContext {
double responsiveWidth(double size) {
return size * (MediaQuery.of(this).size.width / 375);
}
double responsiveHeight(double size) {
return size * (MediaQuery.of(this).size.height / 812);
}
double responsiveText(double size) {
final width = MediaQuery.of(this).size.width;
if (width < 350) return size * 0.9;
if (width > 600) return size * 1.2;
return size;
}
}
class VoiceTransactionDialog extends StatefulWidget {
const VoiceTransactionDialog({super.key});
@ -18,32 +34,82 @@ class VoiceTransactionDialog extends StatefulWidget {
}
class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
with SingleTickerProviderStateMixin {
with TickerProviderStateMixin {
late stt.SpeechToText _speech;
bool _isListening = false;
String _spokenText = '';
bool _processing = false;
// Animation controllers
late AnimationController _pulseController;
late AnimationController _waveController;
late AnimationController _slideController;
late AnimationController _fadeController;
// Animations
late Animation<double> _pulseAnimation;
late Animation<double> _waveAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_speech = stt.SpeechToText();
// Initialize animation controllers
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
duration: const Duration(milliseconds: 1500),
);
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
_waveController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
_slideController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
// Initialize animations
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
_waveAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _waveController, curve: Curves.elasticOut),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeOut));
// Start initial animations
_slideController.forward();
_fadeController.forward();
}
@override
void dispose() {
_pulseController.dispose();
_waveController.dispose();
_slideController.dispose();
_fadeController.dispose();
super.dispose();
}
@ -57,10 +123,13 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
_processing = true;
});
_pulseController.stop();
_waveController.stop();
await _speech.stop();
if (_spokenText.isNotEmpty) {
await _sendToBackend();
}
setState(() {
_processing = false;
});
@ -69,10 +138,7 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
onError: (val) {
_showCustomSnackBar(
message: 'Speech recognition error: $val',
icon: Icons.mic_off,
backgroundColor: const Color(0xFFFF6B6B),
iconColor: Colors.white,
duration: const Duration(seconds: 3),
isError: true,
);
},
);
@ -82,7 +148,9 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
_isListening = true;
_spokenText = '';
});
_pulseController.repeat(reverse: true);
_waveController.repeat();
_speech.listen(
onResult: (val) {
@ -97,7 +165,9 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
_isListening = false;
_processing = true;
});
_pulseController.stop();
_waveController.stop();
_speech.stop();
}
}
@ -132,57 +202,94 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
throw Exception('Save failed with status: ${response.statusCode}');
}
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: ${e.toString()}')));
_showCustomSnackBar(message: 'Error: ${e.toString()}', isError: true);
}
}
void _showCustomSnackBar({
required String message,
required IconData icon,
required Color backgroundColor,
required Color iconColor,
required Duration duration,
}) {
void _showCustomSnackBar({required String message, bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(icon, color: iconColor),
const SizedBox(width: 12),
Icon(
isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white,
size: context.responsiveWidth(20),
),
SizedBox(width: context.responsiveWidth(12)),
Expanded(
child: Text(
message,
style: GoogleFonts.inter(
color: Colors.white,
fontSize: context.responsiveText(14),
fontWeight: FontWeight.w500,
),
),
),
],
),
backgroundColor: backgroundColor,
duration: duration,
backgroundColor:
isError ? const Color(0xFFFF6B6B) : const Color(0xFF4CAF50),
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(context.responsiveWidth(16)),
),
margin: EdgeInsets.all(context.responsiveWidth(16)),
),
);
}
Widget _buildWaveIndicator() {
return AnimatedBuilder(
animation: _waveAnimation,
builder: (context, child) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
final delay = index * 0.2;
final animValue = (_waveAnimation.value - delay).clamp(0.0, 1.0);
final height = 4 + (animValue * 20);
return Container(
margin: EdgeInsets.symmetric(
horizontal: context.responsiveWidth(2),
),
width: context.responsiveWidth(4),
height: context.responsiveHeight(height),
decoration: BoxDecoration(
color: accentColor.withOpacity(0.8),
borderRadius: BorderRadius.circular(context.responsiveWidth(2)),
),
);
}),
);
},
);
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.85, // Reduced height
return AnimatedBuilder(
animation: Listenable.merge([_slideController, _fadeController]),
builder: (context, child) {
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
height: context.responsiveHeight(680),
decoration: BoxDecoration(
color: cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
borderRadius: BorderRadius.vertical(
top: Radius.circular(context.responsiveWidth(32)),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 32,
offset: const Offset(0, -12),
color: Colors.black.withOpacity(0.1),
blurRadius: context.responsiveWidth(40),
offset: Offset(0, -context.responsiveHeight(8)),
),
],
),
@ -190,39 +297,50 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
children: [
// Handle Bar
Container(
width: 48,
height: 4,
margin: const EdgeInsets.only(top: 16, bottom: 8),
width: context.responsiveWidth(48),
height: context.responsiveHeight(4),
margin: EdgeInsets.only(
top: context.responsiveHeight(20),
bottom: context.responsiveHeight(8),
),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(2),
borderRadius: BorderRadius.circular(
context.responsiveWidth(2),
),
),
),
// Header
Padding(
padding: const EdgeInsets.fromLTRB(
24,
16,
24,
16,
), // Added bottom padding
padding: EdgeInsets.fromLTRB(
context.responsiveWidth(24),
context.responsiveHeight(20),
context.responsiveWidth(24),
context.responsiveHeight(20),
),
child: Row(
children: [
Container(
width: 52,
height: 52,
width: context.responsiveWidth(56),
height: context.responsiveWidth(56),
decoration: BoxDecoration(
color: accentColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(
context.responsiveWidth(18),
),
border: Border.all(
color: accentColor.withOpacity(0.2),
color: accentColor.withOpacity(0.3),
width: 1.5,
),
),
child: Icon(Icons.mic_outlined, color: accentColor, size: 28),
child: Icon(
Icons.mic_outlined,
color: accentColor,
size: context.responsiveWidth(28),
),
const SizedBox(width: 16),
),
SizedBox(width: context.responsiveWidth(16)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -231,16 +349,17 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
'Voice Transaction',
style: GoogleFonts.inter(
color: primaryColor,
fontSize: 20, // Reduced font size
fontSize: context.responsiveText(22),
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
),
),
const SizedBox(height: 4), // Added spacing
SizedBox(height: context.responsiveHeight(6)),
Text(
'Speak naturally to record your transaction',
style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6),
fontSize: 12, // Reduced font size
color: primaryColor.withOpacity(0.65),
fontSize: context.responsiveText(14),
fontWeight: FontWeight.w500,
),
),
@ -251,28 +370,47 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
),
),
// Main content with SingleChildScrollView to prevent overflow
// Main Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(
horizontal: context.responsiveWidth(24),
vertical: context.responsiveHeight(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Status Card
// Voice Input Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
padding: EdgeInsets.all(
context.responsiveWidth(24),
),
decoration: BoxDecoration(
color:
gradient:
_isListening
? accentColor.withOpacity(0.08)
: backgroundColor,
borderRadius: BorderRadius.circular(20),
? LinearGradient(
colors: [
accentColor.withOpacity(0.05),
accentColor.withOpacity(0.12),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: LinearGradient(
colors: [
backgroundColor,
backgroundColor.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(
context.responsiveWidth(24),
),
border: Border.all(
color:
_isListening
? accentColor.withOpacity(0.2)
: primaryColor.withOpacity(0.1),
? accentColor.withOpacity(0.3)
: primaryColor.withOpacity(0.08),
width: 1.5,
),
),
@ -283,57 +421,105 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
animation: _pulseAnimation,
builder: (context, child) {
return Transform.scale(
scale: _isListening ? _pulseAnimation.value : 1.0,
scale:
_isListening
? _pulseAnimation.value
: 1.0,
child: GestureDetector(
onTap: _processing ? null : _listen,
child: Container(
width: 80, // Reduced size
height: 80, // Reduced size
width: context.responsiveWidth(96),
height: context.responsiveWidth(96),
decoration: BoxDecoration(
color:
gradient:
_isListening
? accentColor.withOpacity(0.15)
: primaryColor.withOpacity(0.05),
? RadialGradient(
colors: [
accentColor.withOpacity(
0.2,
),
accentColor.withOpacity(
0.05,
),
],
)
: RadialGradient(
colors: [
primaryColor
.withOpacity(0.08),
primaryColor
.withOpacity(0.02),
],
),
shape: BoxShape.circle,
border: Border.all(
color:
_isListening
? accentColor.withOpacity(0.4)
: primaryColor.withOpacity(0.15),
? accentColor.withOpacity(
0.4,
)
: primaryColor
.withOpacity(0.2),
width: 2.5,
),
boxShadow:
_isListening
? [
BoxShadow(
color: accentColor.withOpacity(
0.2,
color: accentColor
.withOpacity(0.3),
blurRadius: context
.responsiveWidth(
24,
),
blurRadius: 20,
spreadRadius: 0,
),
]
: null,
: [
BoxShadow(
color: primaryColor
.withOpacity(0.1),
blurRadius: context
.responsiveWidth(
12,
),
offset: Offset(
0,
context
.responsiveHeight(
4,
),
),
),
],
),
child:
_processing
? const CircularProgressIndicator(
? SizedBox(
width: context
.responsiveWidth(32),
height: context
.responsiveWidth(32),
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.blue,
AlwaysStoppedAnimation<
Color
>(accentColor),
),
)
: Icon(
_isListening
? Icons.mic
: Icons.mic_none,
size: 36, // Reduced size
size: context
.responsiveWidth(42),
color:
_isListening
? accentColor
: primaryColor.withOpacity(
0.6,
: primaryColor
.withOpacity(
0.7,
),
),
),
@ -341,94 +527,156 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
);
},
),
const SizedBox(height: 16), // Reduced spacing
SizedBox(height: context.responsiveHeight(20)),
// Wave Indicator (only when listening)
if (_isListening) ...[
_buildWaveIndicator(),
SizedBox(
height: context.responsiveHeight(16),
),
],
// Status Text
Text(
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
_processing
? 'Processing your request...'
: _isListening
? 'Listening... Tap to stop'
: 'Tap microphone to start recording',
key: ValueKey(
_processing
? 'processing'
: _isListening
? 'listening'
: 'idle',
),
style: GoogleFonts.inter(
color:
_isListening || _processing
? accentColor
: primaryColor.withOpacity(0.7),
fontSize: 14, // Reduced font size
fontSize: context.responsiveText(16),
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
textAlign: TextAlign.center,
),
),
],
),
),
const SizedBox(height: 20), // Reduced spacing
// Spoken Text Display
if (_spokenText.isNotEmpty) ...[
Container(
SizedBox(height: context.responsiveHeight(24)),
// Transcript Display
AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
height: _spokenText.isNotEmpty ? null : 0,
child:
_spokenText.isNotEmpty
? Container(
width: double.infinity,
padding: const EdgeInsets.all(16), // Reduced padding
padding: EdgeInsets.all(
context.responsiveWidth(20),
),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(
16,
), // Reduced radius
context.responsiveWidth(20),
),
border: Border.all(
color: accentColor.withOpacity(0.15),
color: accentColor.withOpacity(0.2),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: accentColor.withOpacity(0.05),
blurRadius: 8, // Reduced blur
offset: const Offset(0, 2), // Reduced offset
color: accentColor.withOpacity(
0.08,
),
blurRadius: context.responsiveWidth(
16,
),
offset: Offset(
0,
context.responsiveHeight(4),
),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.email, color: accentColor, size: 16),
const SizedBox(width: 8),
Icon(
Icons.transcribe_outlined,
color: accentColor,
size: context.responsiveWidth(
18,
),
),
SizedBox(
width: context.responsiveWidth(
8,
),
),
Text(
'Transcript',
style: GoogleFonts.inter(
color: accentColor,
fontSize: 12,
fontSize: context
.responsiveText(13),
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
letterSpacing: 0.8,
),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: context.responsiveHeight(
12,
),
),
Text(
_spokenText,
style: GoogleFonts.inter(
color: primaryColor,
fontSize: 14, // Reduced font size
fontSize: context.responsiveText(
15,
),
fontWeight: FontWeight.w500,
height: 1.4, // Reduced line height
height: 1.5,
letterSpacing: 0.1,
),
maxLines: 5, // Limit lines
overflow: TextOverflow.ellipsis,
),
],
),
)
: const SizedBox.shrink(),
),
const SizedBox(height: 20), // Reduced spacing
],
// Action Button
SizedBox(height: context.responsiveHeight(32)),
// Close Button
Container(
width: double.infinity,
height: 50, // Reduced height
height: context.responsiveHeight(56),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(12), // Reduced radius
gradient: LinearGradient(
colors: [
primaryColor.withOpacity(0.06),
primaryColor.withOpacity(0.12),
],
),
borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
border: Border.all(
color: primaryColor.withOpacity(0.15),
width: 1,
@ -436,17 +684,22 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
onTap: () => Navigator.pop(context),
child: Center(
child: Text(
'Close',
style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.8),
fontSize: 14, // Reduced font size
fontSize: context.responsiveText(16),
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
),
@ -454,13 +707,17 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
),
),
const SizedBox(height: 16), // Added bottom spacing for safety
SizedBox(height: context.responsiveHeight(20)),
],
),
),
),
],
),
),
),
);
},
);
}
}

View file

@ -227,7 +227,7 @@ class _FinTrackHomePageState extends State<FinTrackHomePage>
),
),
// Use bottomNavigationBar instead of floatingActionButton
bottomNavigationBar: CustomNavBar(
floatingActionButton: CustomNavBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() => _selectedIndex = index);
@ -236,6 +236,7 @@ class _FinTrackHomePageState extends State<FinTrackHomePage>
accentColor: accentColor,
primaryColor: primaryColor,
cardColor: cardColor,
floatingActionButtonLocation: FloatingActionButtonLocation.centerTop,
),
);
}

View file

@ -767,6 +767,7 @@ class StatisticsState extends State<Statistics> with TickerProviderStateMixin {
accentColor: accentColor,
primaryColor: primaryColor,
cardColor: cardColor,
floatingActionButtonLocation: FloatingActionButtonLocation.centerTop,
),
);
}