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 // 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/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
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:monarch/main_pages/HomePage/homepage.dart'; import 'package:monarch/main_pages/HomePage/homepage.dart';
import 'package:monarch/main_pages/HomePage/Components/Voice/voice_dialog.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 'package:monarch/other_pages/enviroment.dart';
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
@ -130,7 +17,8 @@ class LoginScreen extends StatefulWidget {
State<LoginScreen> createState() => _LoginScreenState(); State<LoginScreen> createState() => _LoginScreenState();
} }
class _LoginScreenState extends State<LoginScreen> { class _LoginScreenState extends State<LoginScreen>
with TickerProviderStateMixin {
final GoogleSignIn _googleSignIn = GoogleSignIn( final GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: [ scopes: [
'email', 'email',
@ -142,6 +30,88 @@ class _LoginScreenState extends State<LoginScreen> {
); );
bool _isLoading = false; 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 { Future<void> _handleGoogleSignIn() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@ -164,7 +134,6 @@ class _LoginScreenState extends State<LoginScreen> {
return; return;
} }
// Send idToken to backend for login
final uri = Uri.parse("${Environment.baseUrl}/auth/google/token"); final uri = Uri.parse("${Environment.baseUrl}/auth/google/token");
final res = await http.post( final res = await http.post(
uri, uri,
@ -179,18 +148,9 @@ class _LoginScreenState extends State<LoginScreen> {
prefs.setString('userId', data['user']['_id']); prefs.setString('userId', data['user']['_id']);
prefs.setString('accessToken', accessToken); prefs.setString('accessToken', accessToken);
// Navigate directly to EmailsScreen with accessToken
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (_) => FinTrackHomePage()),
builder: (_) => FinTrackHomePage(),
// builder:
// (_) => EmailsScreen(
// accessToken: accessToken,
// userId: data['user']['_id'],
// ),
//builder: (_) => AutoSyncEmailsScreen(accessToken: accessToken,userId: data['user']['_id'],),
),
); );
} else { } else {
debugPrint("❌ Login failed: ${data['error']}"); debugPrint("❌ Login failed: ${data['error']}");
@ -207,32 +167,347 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, body: Container(
body: Center( 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: child:
_isLoading _isLoading
? const CircularProgressIndicator() ? Container(
: ElevatedButton.icon( width: double.infinity,
style: ElevatedButton.styleFrom( height: 60,
backgroundColor: Colors.white, decoration: BoxDecoration(
foregroundColor: Colors.black, color: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 20,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30), 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:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:monarch/login.dart'; 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/support/add.dart';
import 'package:monarch/main_pages/Statistics/Budget/update_budget.dart'; import 'package:monarch/main_pages/Statistics/Budget/update_budget.dart';
import 'package:monarch/main_pages/HomePage/homepage.dart'; import 'package:monarch/main_pages/HomePage/homepage.dart';

View file

@ -1,15 +1,31 @@
// ignore_for_file: deprecated_member_use, unused_import // ignore_for_file: deprecated_member_use, unused_import
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:monarch/main_pages/HomePage/homepage.dart'; import 'package:monarch/main_pages/HomePage/homepage.dart';
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math;
import 'package:monarch/other_pages/colors.dart'; import 'package:monarch/other_pages/colors.dart';
import 'package:monarch/other_pages/enviroment.dart'; import 'package:monarch/other_pages/enviroment.dart';
import 'package:shared_preferences/shared_preferences.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 { class EmailTransactionDialog extends StatefulWidget {
const EmailTransactionDialog({super.key}); const EmailTransactionDialog({super.key});
@ -17,7 +33,8 @@ class EmailTransactionDialog extends StatefulWidget {
State<EmailTransactionDialog> createState() => _EmailTransactionDialogState(); State<EmailTransactionDialog> createState() => _EmailTransactionDialogState();
} }
class _EmailTransactionDialogState extends State<EmailTransactionDialog> { class _EmailTransactionDialogState extends State<EmailTransactionDialog>
with TickerProviderStateMixin {
final List<dynamic> _emails = []; final List<dynamic> _emails = [];
bool _loading = true; bool _loading = true;
String? _status; String? _status;
@ -25,13 +42,98 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
String? _error; String? _error;
String? _userId; String? _userId;
String? _accessToken; 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 @override
void initState() { void initState() {
super.initState(); super.initState();
_initAnimations();
_startEntryAnimation();
_autoSyncEmails(); _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 { Future<void> _autoSyncEmails() async {
setState(() { setState(() {
_loading = true; _loading = true;
@ -39,6 +141,9 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
_syncedCount = 0; _syncedCount = 0;
}); });
_rotationController.repeat();
_progressController.forward();
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('userId'); final userId = prefs.getString('userId');
@ -52,7 +157,6 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (data['success'] == true && data['count'] != null) { if (data['success'] == true && data['count'] != null) {
setState(() { setState(() {
_loading = false; _loading = false;
@ -60,216 +164,442 @@ class _EmailTransactionDialogState extends State<EmailTransactionDialog> {
_status = "✅ Synced $_syncedCount transactions successfully!"; _status = "✅ Synced $_syncedCount transactions successfully!";
}); });
// // Optional: navigate back or to another screen after a delay _rotationController.stop();
// Future.delayed(const Duration(seconds: 2), () { _progressController.reset();
// Navigator.pushReplacement(
// context, // Success animation
// // MaterialPageRoute(builder: (_) => const FinTrackHomePage()), _scaleController.reset();
// ); _scaleController.forward();
// });
} else { } else {
setState(() { setState(() {
_loading = false; _loading = false;
_status = "❌ Sync failed: Unexpected response format"; _status = "❌ Sync failed: Unexpected response format";
}); });
_rotationController.stop();
} }
} else { } else {
setState(() { setState(() {
_loading = false; _loading = false;
_status = "❌ Backend error: ${response.body}"; _status = "❌ Backend error: ${response.body}";
}); });
_rotationController.stop();
} }
} catch (error) { } catch (error) {
setState(() { setState(() {
_loading = false; _loading = false;
_status = "⚠️ Network or unexpected error: $error"; _status = "⚠️ Network or unexpected error: $error";
}); });
_rotationController.stop();
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return SlideTransition(
height: MediaQuery.of(context).size.height * 0.7, position: _slideAnimation,
child: Container(
height: context.responsiveHeight(560),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardColor, color: cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), borderRadius: BorderRadius.vertical(
top: Radius.circular(context.responsiveWidth(28)),
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withOpacity(0.08),
blurRadius: 32, blurRadius: 32,
offset: const Offset(0, -8), offset: const Offset(0, -8),
spreadRadius: 2,
),
BoxShadow(
color: primaryColor.withOpacity(0.05),
blurRadius: 16,
offset: const Offset(0, -4),
), ),
], ],
), ),
child: Column( child: Column(
children: [ children: [
// Top drag handle // Drag Handle with Animation
Container( FadeTransition(
width: 48, opacity: _fadeAnimation,
height: 4, child: Container(
margin: const EdgeInsets.only(top: 16, bottom: 24), width: context.responsiveWidth(48),
height: context.responsiveHeight(4),
margin: EdgeInsets.only(
top: context.responsiveHeight(16),
bottom: context.responsiveHeight(24),
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: primaryColor.withOpacity(0.2), color: primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(
context.responsiveWidth(2),
),
),
), ),
), ),
// Header // Animated Header
Padding( FadeTransition(
padding: const EdgeInsets.symmetric(horizontal: 24), opacity: _fadeAnimation,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: context.responsiveWidth(24),
),
child: Row( child: Row(
children: [ children: [
Container( // Animated Icon Container
width: 48, ScaleTransition(
height: 48, scale: _scaleAnimation,
child: Container(
width: context.responsiveWidth(56),
height: context.responsiveWidth(56),
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor.withOpacity(0.1), gradient: LinearGradient(
borderRadius: BorderRadius.circular(12), colors: [
accentColor.withOpacity(0.1),
accentColor.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
border: Border.all( border: Border.all(
color: accentColor.withOpacity(0.2), 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), SizedBox(width: context.responsiveWidth(16)),
Column(
// Title Section
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Email Transactions', 'Email Transactions',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor, color: primaryColor,
fontSize: 20, fontSize: context.responsiveText(20),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: -0.5,
), ),
), ),
SizedBox(height: context.responsiveHeight(2)),
Text( Text(
'Forward receipts to auto-add transactions', 'Forward receipts to auto-add transactions',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6), color: primaryColor.withOpacity(0.6),
fontSize: 14, fontSize: context.responsiveText(13),
fontWeight: FontWeight.w500, 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) // Animated Refresh Button
Expanded( ScaleTransition(
child: scale: _scaleAnimation,
_loading child: Container(
? const Center(child: CircularProgressIndicator()) width: context.responsiveWidth(44),
: _error != null height: context.responsiveWidth(44),
? 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,
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor.withOpacity(0.1), 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( child: Icon(
Icons.receipt_long, Icons.refresh_rounded,
color: accentColor, color: accentColor,
size: 22, size: context.responsiveWidth(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),
), ),
); );
}, },
), ),
), ),
),
),
),
],
),
),
),
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 // ignore_for_file: deprecated_member_use
import 'dart:ui';
import 'package:flutter/material.dart'; 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 { class CustomNavBar extends StatelessWidget {
final int currentIndex; final int currentIndex;
final Function(int) onTap; final void Function(int) onTap;
final Color backgroundColor; final Color backgroundColor;
final Color accentColor; final Color accentColor;
final Color primaryColor; final Color primaryColor;
@ -18,88 +22,103 @@ class CustomNavBar extends StatelessWidget {
required this.accentColor, required this.accentColor,
required this.primaryColor, required this.primaryColor,
required this.cardColor, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Align(
height: 80, // Fixed height for bottom navigation 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( decoration: BoxDecoration(
color: backgroundColor, color: cardColor.withOpacity(0.65),
borderRadius: BorderRadius.circular(40),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: primaryColor.withOpacity(0.15),
blurRadius: 10, blurRadius: 30,
offset: const Offset(0, -2), offset: const Offset(0, 8),
), ),
], ],
), ),
child: ClipRRect(
borderRadius: BorderRadius.circular(40),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children:
_buildNavItem( _navItems
icon: Icons.home_outlined, .map((item) => _buildNavItem(context, item))
activeIcon: Icons.home_rounded, .toList(),
label: 'Home',
index: 0,
), ),
_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({ Widget _buildNavItem(BuildContext context, NavItem item) {
required IconData icon, final isActive = currentIndex == item.index;
required IconData activeIcon, return GestureDetector(
required String label, onTap: () {
required int index, onTap(item.index);
}) { Navigator.pushReplacement(
final isActive = currentIndex == index; context,
MaterialPageRoute(builder: (context) => item.page),
return Expanded( );
child: Material( },
color: Colors.transparent,
child: InkWell(
onTap: () => onTap(index),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( AnimatedContainer(
isActive ? activeIcon : icon, duration: const Duration(milliseconds: 250),
color: isActive ? accentColor : primaryColor.withOpacity(0.6), 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, size: 24,
), ),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( AnimatedContainer(
label, duration: const Duration(milliseconds: 250),
style: TextStyle( width: 6,
color: isActive ? accentColor : primaryColor.withOpacity(0.6), height: 6,
fontSize: 12, decoration: BoxDecoration(
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.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/support/add.dart';
import 'package:monarch/other_pages/colors.dart'; import 'package:monarch/other_pages/colors.dart';
@ -66,8 +67,13 @@ class QuickActionsPage extends StatelessWidget {
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
HapticFeedback.lightImpact(); HapticFeedback.heavyImpact();
// Navigate to Statistics Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const Statistics(),
),
);
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(20), 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:monarch/other_pages/enviroment.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class ConfirmTransactionPage extends StatelessWidget { class ConfirmTransactionPage extends StatefulWidget {
final Map<String, dynamic> data; final Map<String, dynamic> data;
const ConfirmTransactionPage({super.key, required this.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 { Future<void> _saveTransaction(BuildContext context) async {
const String saveUrl = const String saveUrl = '${Environment.baseUrl}/api/transaction/add';
'${Environment.baseUrl}/api/transaction/add'; // ✅ make sure this matches backend
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -31,12 +67,10 @@ class ConfirmTransactionPage extends StatelessWidget {
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode({ body: jsonEncode({
'userId': userId, 'userId': userId,
'description': data['description'] ?? '', 'description': widget.data['description'] ?? '',
'amount': 'amount': double.tryParse(widget.data['amount'].toString()) ?? 0.0,
double.tryParse(data['amount'].toString()) ?? 'category': widget.data['category'] ?? 'Uncategorized',
0.0, // ✅ force number 'date': widget.data['date'] ?? DateTime.now().toIso8601String(),
'category': data['category'] ?? 'Uncategorized',
'date': data['date'] ?? DateTime.now().toIso8601String(),
}), }),
); );
@ -47,11 +81,9 @@ class ConfirmTransactionPage extends StatelessWidget {
icon: Icons.check_circle, icon: Icons.check_circle,
backgroundColor: const Color(0xFF4CAF50), backgroundColor: const Color(0xFF4CAF50),
iconColor: Colors.white, iconColor: Colors.white,
duration: const Duration(seconds: 3),
); );
Navigator.popUntil(context, (route) => route.isFirst); Navigator.popUntil(context, (route) => route.isFirst);
} else { } else {
// 👇 Log backend response
throw Exception('Save failed: ${response.statusCode} ${response.body}'); throw Exception('Save failed: ${response.statusCode} ${response.body}');
} }
} catch (e) { } catch (e) {
@ -61,7 +93,6 @@ class ConfirmTransactionPage extends StatelessWidget {
icon: Icons.error_outline, icon: Icons.error_outline,
backgroundColor: const Color(0xFFFF6B6B), backgroundColor: const Color(0xFFFF6B6B),
iconColor: Colors.white, iconColor: Colors.white,
duration: const Duration(seconds: 4),
); );
} }
} }
@ -76,47 +107,29 @@ class ConfirmTransactionPage extends StatelessWidget {
}) { }) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Container( content: Row(
padding: EdgeInsets.symmetric(vertical: context.responsiveHeight(4)),
child: Row(
children: [ children: [
Container( Icon(icon, color: iconColor, size: context.responsiveText(20)),
padding: EdgeInsets.all(context.responsiveWidth(8)), SizedBox(width: 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)),
Expanded( Expanded(
child: Text( child: Text(
message, message,
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w600,
color: Colors.white, color: Colors.white,
fontSize: context.responsiveText(14), fontSize: context.responsiveText(14),
height: 1.4,
), ),
), ),
), ),
], ],
), ),
),
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(context.responsiveWidth(12)),
duration: duration,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(context.responsiveWidth(12)), 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( return Row(
children: [ children: [
Container( Container(
width: 40, width: context.responsiveWidth(40),
height: 40, height: context.responsiveWidth(40),
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor.withOpacity(0.1), color: accentColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(10), 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( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -147,17 +164,16 @@ class ConfirmTransactionPage extends StatelessWidget {
label, label,
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6), color: primaryColor.withOpacity(0.6),
fontSize: 12, fontSize: context.responsiveText(12),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w500,
letterSpacing: 0.5,
), ),
), ),
const SizedBox(height: 4), SizedBox(height: context.responsiveHeight(2)),
Text( Text(
value, value,
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: isAmount ? Colors.green : primaryColor, color: isAmount ? Colors.green : primaryColor,
fontSize: 16, fontSize: context.responsiveText(16),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
@ -170,12 +186,12 @@ class ConfirmTransactionPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final amount = data['amount']; final amount = widget.data['amount'];
final description = data['description']; final description = widget.data['description'];
final category = data['category']; final category = widget.data['category'];
final date = final date =
data['date'] != null widget.data['date'] != null
? DateTime.parse(data['date']).toLocal() ? DateTime.parse(widget.data['date']).toLocal()
: DateTime.now(); : DateTime.now();
return Scaffold( return Scaffold(
@ -184,11 +200,10 @@ class ConfirmTransactionPage extends StatelessWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
leading: Container( leading: Container(
margin: const EdgeInsets.all(8), margin: EdgeInsets.all(context.responsiveWidth(6)),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardColor, color: cardColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(context.responsiveWidth(12)),
border: Border.all(color: primaryColor.withOpacity(0.1), width: 1),
), ),
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_back, color: primaryColor), icon: Icon(Icons.arrow_back, color: primaryColor),
@ -199,81 +214,72 @@ class ConfirmTransactionPage extends StatelessWidget {
'Confirm Transaction', 'Confirm Transaction',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor, color: primaryColor,
fontSize: 20, fontSize: context.responsiveText(18),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
centerTitle: true, centerTitle: true,
), ),
body: SafeArea( body: SafeArea(
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: EdgeInsets.all(context.responsiveWidth(20)),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header Card // Header
Container( Container(
width: double.infinity, padding: EdgeInsets.all(context.responsiveWidth(18)),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardColor, color: cardColor,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(
context.responsiveWidth(18),
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: accentColor.withOpacity(0.05), color: accentColor.withOpacity(0.08),
blurRadius: 20, blurRadius: 18,
offset: const Offset(0, 8), offset: const Offset(0, 6),
), ),
], ],
border: Border.all(
color: accentColor.withOpacity(0.1),
width: 1.5,
), ),
), child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Container( Container(
width: 48, padding: EdgeInsets.all(context.responsiveWidth(10)),
height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1), color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
border: Border.all( context.responsiveWidth(10),
color: Colors.green.withOpacity(0.2),
width: 1.5,
), ),
), ),
child: const Icon( child: Icon(
Icons.check_circle_outline, Icons.check_circle_outline,
color: Colors.green, color: Colors.green,
size: 24, size: context.responsiveText(22),
), ),
), ),
const SizedBox(width: 16), SizedBox(width: context.responsiveWidth(14)),
Expanded( Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Review Transaction', "Review Transaction",
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor, fontSize: context.responsiveText(16),
fontSize: 18,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: primaryColor,
), ),
), ),
Text( Text(
'Please confirm the details below', "Please confirm the details below",
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6), fontSize: context.responsiveText(13),
fontSize: 14,
fontWeight: FontWeight.w500, 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( Container(
width: double.infinity, padding: EdgeInsets.all(context.responsiveWidth(20)),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(
context.responsiveWidth(18),
),
border: Border.all( border: Border.all(
color: primaryColor.withOpacity(0.1), color: primaryColor.withOpacity(0.08),
width: 1, width: 1,
), ),
), ),
@ -300,33 +307,27 @@ class ConfirmTransactionPage extends StatelessWidget {
children: [ children: [
_buildDetailItem( _buildDetailItem(
icon: Icons.description_outlined, icon: Icons.description_outlined,
label: 'Description', label: "Description",
value: description?.toString() ?? 'No description', value: description?.toString() ?? "No description",
), ),
SizedBox(height: context.responsiveHeight(16)),
const SizedBox(height: 20),
_buildDetailItem( _buildDetailItem(
icon: Icons.payments_outlined, icon: Icons.payments_outlined,
label: 'Amount', label: "Amount",
value: '₹$amount', value: "₹$amount",
isAmount: true, isAmount: true,
), ),
SizedBox(height: context.responsiveHeight(16)),
const SizedBox(height: 20),
_buildDetailItem( _buildDetailItem(
icon: Icons.category_outlined, icon: Icons.category_outlined,
label: 'Category', label: "Category",
value: category?.toString() ?? 'Uncategorized', value: category?.toString() ?? "Uncategorized",
), ),
SizedBox(height: context.responsiveHeight(16)),
const SizedBox(height: 20),
_buildDetailItem( _buildDetailItem(
icon: Icons.calendar_today_outlined, icon: Icons.calendar_today_outlined,
label: 'Date', label: "Date",
value: '${date.day}/${date.month}/${date.year}', value: "${date.day}/${date.month}/${date.year}",
), ),
], ],
), ),
@ -334,82 +335,77 @@ class ConfirmTransactionPage extends StatelessWidget {
const Spacer(), const Spacer(),
// Action Buttons // Buttons
Column( Column(
children: [ children: [
// Confirm Button // Confirm
Container( GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) => _saveTransaction(context),
child: AnimatedScale(
scale: 1,
duration: const Duration(milliseconds: 150),
child: Container(
width: double.infinity, width: double.infinity,
height: 56, padding: EdgeInsets.symmetric(
vertical: context.responsiveHeight(14),
),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [Colors.green, Colors.green.shade600], colors: [Colors.green, Colors.green.shade700],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(
context.responsiveWidth(14),
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.green.withOpacity(0.3), color: Colors.green.withOpacity(0.3),
blurRadius: 12, blurRadius: 10,
offset: const Offset(0, 4), 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: 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( child: Text(
'Edit or Cancel', "Confirm & Save Transaction",
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.7), fontSize: context.responsiveText(15),
fontSize: 16, 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, 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 // ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api, use_build_context_synchronously, deprecated_member_use
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http; 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:shared_preferences/shared_preferences.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt; import 'package:speech_to_text/speech_to_text.dart' as stt;
import 'package:monarch/other_pages/colors.dart'; import 'package:monarch/other_pages/colors.dart';
import 'package:monarch/other_pages/enviroment.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 { class VoiceTransactionDialog extends StatefulWidget {
const VoiceTransactionDialog({super.key}); const VoiceTransactionDialog({super.key});
@ -18,32 +34,82 @@ class VoiceTransactionDialog extends StatefulWidget {
} }
class _VoiceTransactionDialogState extends State<VoiceTransactionDialog> class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
with SingleTickerProviderStateMixin { with TickerProviderStateMixin {
late stt.SpeechToText _speech; late stt.SpeechToText _speech;
bool _isListening = false; bool _isListening = false;
String _spokenText = ''; String _spokenText = '';
bool _processing = false; bool _processing = false;
// Animation controllers
late AnimationController _pulseController; late AnimationController _pulseController;
late AnimationController _waveController;
late AnimationController _slideController;
late AnimationController _fadeController;
// Animations
late Animation<double> _pulseAnimation; late Animation<double> _pulseAnimation;
late Animation<double> _waveAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_speech = stt.SpeechToText(); _speech = stt.SpeechToText();
// Initialize animation controllers
_pulseController = AnimationController( _pulseController = AnimationController(
vsync: this, 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), 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 @override
void dispose() { void dispose() {
_pulseController.dispose(); _pulseController.dispose();
_waveController.dispose();
_slideController.dispose();
_fadeController.dispose();
super.dispose(); super.dispose();
} }
@ -57,10 +123,13 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
_processing = true; _processing = true;
}); });
_pulseController.stop(); _pulseController.stop();
_waveController.stop();
await _speech.stop(); await _speech.stop();
if (_spokenText.isNotEmpty) { if (_spokenText.isNotEmpty) {
await _sendToBackend(); await _sendToBackend();
} }
setState(() { setState(() {
_processing = false; _processing = false;
}); });
@ -69,10 +138,7 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
onError: (val) { onError: (val) {
_showCustomSnackBar( _showCustomSnackBar(
message: 'Speech recognition error: $val', message: 'Speech recognition error: $val',
icon: Icons.mic_off, isError: true,
backgroundColor: const Color(0xFFFF6B6B),
iconColor: Colors.white,
duration: const Duration(seconds: 3),
); );
}, },
); );
@ -82,7 +148,9 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
_isListening = true; _isListening = true;
_spokenText = ''; _spokenText = '';
}); });
_pulseController.repeat(reverse: true); _pulseController.repeat(reverse: true);
_waveController.repeat();
_speech.listen( _speech.listen(
onResult: (val) { onResult: (val) {
@ -97,7 +165,9 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
_isListening = false; _isListening = false;
_processing = true; _processing = true;
}); });
_pulseController.stop(); _pulseController.stop();
_waveController.stop();
_speech.stop(); _speech.stop();
} }
} }
@ -132,57 +202,94 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
throw Exception('Save failed with status: ${response.statusCode}'); throw Exception('Save failed with status: ${response.statusCode}');
} }
} catch (e) { } catch (e) {
ScaffoldMessenger.of( _showCustomSnackBar(message: 'Error: ${e.toString()}', isError: true);
context,
).showSnackBar(SnackBar(content: Text('Error: ${e.toString()}')));
} }
} }
void _showCustomSnackBar({ void _showCustomSnackBar({required String message, bool isError = false}) {
required String message,
required IconData icon,
required Color backgroundColor,
required Color iconColor,
required Duration duration,
}) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Row( content: Row(
children: [ children: [
Icon(icon, color: iconColor), Icon(
const SizedBox(width: 12), isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white,
size: context.responsiveWidth(20),
),
SizedBox(width: context.responsiveWidth(12)),
Expanded( Expanded(
child: Text( child: Text(
message, message,
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Colors.white, color: Colors.white,
fontSize: context.responsiveText(14),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ),
], ],
), ),
backgroundColor: backgroundColor, backgroundColor:
duration: duration, isError ? const Color(0xFFFF6B6B) : const Color(0xFF4CAF50),
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
margin: const EdgeInsets.all(16), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return AnimatedBuilder(
height: MediaQuery.of(context).size.height * 0.85, // Reduced height animation: Listenable.merge([_slideController, _fadeController]),
builder: (context, child) {
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
height: context.responsiveHeight(680),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardColor, color: cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), borderRadius: BorderRadius.vertical(
top: Radius.circular(context.responsiveWidth(32)),
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.15), color: Colors.black.withOpacity(0.1),
blurRadius: 32, blurRadius: context.responsiveWidth(40),
offset: const Offset(0, -12), offset: Offset(0, -context.responsiveHeight(8)),
), ),
], ],
), ),
@ -190,39 +297,50 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
children: [ children: [
// Handle Bar // Handle Bar
Container( Container(
width: 48, width: context.responsiveWidth(48),
height: 4, height: context.responsiveHeight(4),
margin: const EdgeInsets.only(top: 16, bottom: 8), margin: EdgeInsets.only(
top: context.responsiveHeight(20),
bottom: context.responsiveHeight(8),
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: primaryColor.withOpacity(0.2), color: primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(
context.responsiveWidth(2),
),
), ),
), ),
// Header // Header
Padding( Padding(
padding: const EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
24, context.responsiveWidth(24),
16, context.responsiveHeight(20),
24, context.responsiveWidth(24),
16, context.responsiveHeight(20),
), // Added bottom padding ),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 52, width: context.responsiveWidth(56),
height: 52, height: context.responsiveWidth(56),
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor.withOpacity(0.1), color: accentColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(
context.responsiveWidth(18),
),
border: Border.all( border: Border.all(
color: accentColor.withOpacity(0.2), color: accentColor.withOpacity(0.3),
width: 1.5, 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( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -231,16 +349,17 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
'Voice Transaction', 'Voice Transaction',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor, color: primaryColor,
fontSize: 20, // Reduced font size fontSize: context.responsiveText(22),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: -0.5,
), ),
), ),
const SizedBox(height: 4), // Added spacing SizedBox(height: context.responsiveHeight(6)),
Text( Text(
'Speak naturally to record your transaction', 'Speak naturally to record your transaction',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.6), color: primaryColor.withOpacity(0.65),
fontSize: 12, // Reduced font size fontSize: context.responsiveText(14),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@ -251,28 +370,47 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
), ),
), ),
// Main content with SingleChildScrollView to prevent overflow // Main Content
Expanded( Expanded(
child: SingleChildScrollView( 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( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
// Status Card // Voice Input Card
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(20), padding: EdgeInsets.all(
context.responsiveWidth(24),
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: gradient:
_isListening _isListening
? accentColor.withOpacity(0.08) ? LinearGradient(
: backgroundColor, colors: [
borderRadius: BorderRadius.circular(20), 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( border: Border.all(
color: color:
_isListening _isListening
? accentColor.withOpacity(0.2) ? accentColor.withOpacity(0.3)
: primaryColor.withOpacity(0.1), : primaryColor.withOpacity(0.08),
width: 1.5, width: 1.5,
), ),
), ),
@ -283,57 +421,105 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
animation: _pulseAnimation, animation: _pulseAnimation,
builder: (context, child) { builder: (context, child) {
return Transform.scale( return Transform.scale(
scale: _isListening ? _pulseAnimation.value : 1.0, scale:
_isListening
? _pulseAnimation.value
: 1.0,
child: GestureDetector( child: GestureDetector(
onTap: _processing ? null : _listen, onTap: _processing ? null : _listen,
child: Container( child: Container(
width: 80, // Reduced size width: context.responsiveWidth(96),
height: 80, // Reduced size height: context.responsiveWidth(96),
decoration: BoxDecoration( decoration: BoxDecoration(
color: gradient:
_isListening _isListening
? accentColor.withOpacity(0.15) ? RadialGradient(
: primaryColor.withOpacity(0.05), colors: [
accentColor.withOpacity(
0.2,
),
accentColor.withOpacity(
0.05,
),
],
)
: RadialGradient(
colors: [
primaryColor
.withOpacity(0.08),
primaryColor
.withOpacity(0.02),
],
),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: color:
_isListening _isListening
? accentColor.withOpacity(0.4) ? accentColor.withOpacity(
: primaryColor.withOpacity(0.15), 0.4,
)
: primaryColor
.withOpacity(0.2),
width: 2.5, width: 2.5,
), ),
boxShadow: boxShadow:
_isListening _isListening
? [ ? [
BoxShadow( BoxShadow(
color: accentColor.withOpacity( color: accentColor
0.2, .withOpacity(0.3),
blurRadius: context
.responsiveWidth(
24,
), ),
blurRadius: 20,
spreadRadius: 0, spreadRadius: 0,
), ),
] ]
: null, : [
BoxShadow(
color: primaryColor
.withOpacity(0.1),
blurRadius: context
.responsiveWidth(
12,
),
offset: Offset(
0,
context
.responsiveHeight(
4,
),
),
),
],
), ),
child: child:
_processing _processing
? const CircularProgressIndicator( ? SizedBox(
width: context
.responsiveWidth(32),
height: context
.responsiveWidth(32),
child: CircularProgressIndicator(
strokeWidth: 3, strokeWidth: 3,
valueColor: valueColor:
AlwaysStoppedAnimation<Color>( AlwaysStoppedAnimation<
Colors.blue, Color
>(accentColor),
), ),
) )
: Icon( : Icon(
_isListening _isListening
? Icons.mic ? Icons.mic
: Icons.mic_none, : Icons.mic_none,
size: 36, // Reduced size size: context
.responsiveWidth(42),
color: color:
_isListening _isListening
? accentColor ? accentColor
: primaryColor.withOpacity( : primaryColor
0.6, .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 // Status Text
Text( AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
_processing _processing
? 'Processing your request...' ? 'Processing your request...'
: _isListening : _isListening
? 'Listening... Tap to stop' ? 'Listening... Tap to stop'
: 'Tap microphone to start recording', : 'Tap microphone to start recording',
key: ValueKey(
_processing
? 'processing'
: _isListening
? 'listening'
: 'idle',
),
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: color:
_isListening || _processing _isListening || _processing
? accentColor ? accentColor
: primaryColor.withOpacity(0.7), : primaryColor.withOpacity(0.7),
fontSize: 14, // Reduced font size fontSize: context.responsiveText(16),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
letterSpacing: 0.2,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
),
], ],
), ),
), ),
const SizedBox(height: 20), // Reduced spacing SizedBox(height: context.responsiveHeight(24)),
// Spoken Text Display
if (_spokenText.isNotEmpty) ...[ // Transcript Display
Container( AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
height: _spokenText.isNotEmpty ? null : 0,
child:
_spokenText.isNotEmpty
? Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), // Reduced padding padding: EdgeInsets.all(
context.responsiveWidth(20),
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardColor, color: cardColor,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
16, context.responsiveWidth(20),
), // Reduced radius ),
border: Border.all( border: Border.all(
color: accentColor.withOpacity(0.15), color: accentColor.withOpacity(0.2),
width: 1.5, width: 1.5,
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: accentColor.withOpacity(0.05), color: accentColor.withOpacity(
blurRadius: 8, // Reduced blur 0.08,
offset: const Offset(0, 2), // Reduced offset ),
blurRadius: context.responsiveWidth(
16,
),
offset: Offset(
0,
context.responsiveHeight(4),
),
), ),
], ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Icon(Icons.email, color: accentColor, size: 16), Icon(
const SizedBox(width: 8), Icons.transcribe_outlined,
color: accentColor,
size: context.responsiveWidth(
18,
),
),
SizedBox(
width: context.responsiveWidth(
8,
),
),
Text( Text(
'Transcript', 'Transcript',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: accentColor, color: accentColor,
fontSize: 12, fontSize: context
.responsiveText(13),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: 0.5, letterSpacing: 0.8,
), ),
), ),
], ],
), ),
const SizedBox(height: 8), SizedBox(
height: context.responsiveHeight(
12,
),
),
Text( Text(
_spokenText, _spokenText,
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor, color: primaryColor,
fontSize: 14, // Reduced font size fontSize: context.responsiveText(
15,
),
fontWeight: FontWeight.w500, 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( Container(
width: double.infinity, width: double.infinity,
height: 50, // Reduced height height: context.responsiveHeight(56),
decoration: BoxDecoration( decoration: BoxDecoration(
color: primaryColor.withOpacity(0.08), gradient: LinearGradient(
borderRadius: BorderRadius.circular(12), // Reduced radius colors: [
primaryColor.withOpacity(0.06),
primaryColor.withOpacity(0.12),
],
),
borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
border: Border.all( border: Border.all(
color: primaryColor.withOpacity(0.15), color: primaryColor.withOpacity(0.15),
width: 1, width: 1,
@ -436,17 +684,22 @@ class _VoiceTransactionDialogState extends State<VoiceTransactionDialog>
), ),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
context.responsiveWidth(16),
),
onTap: () => Navigator.pop(context), onTap: () => Navigator.pop(context),
child: Center( child: Center(
child: Text( child: Text(
'Close', 'Close',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: primaryColor.withOpacity(0.8), color: primaryColor.withOpacity(0.8),
fontSize: 14, // Reduced font size fontSize: context.responsiveText(16),
fontWeight: FontWeight.w600, 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 // Use bottomNavigationBar instead of floatingActionButton
bottomNavigationBar: CustomNavBar( floatingActionButton: CustomNavBar(
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
onTap: (index) { onTap: (index) {
setState(() => _selectedIndex = index); setState(() => _selectedIndex = index);
@ -236,6 +236,7 @@ class _FinTrackHomePageState extends State<FinTrackHomePage>
accentColor: accentColor, accentColor: accentColor,
primaryColor: primaryColor, primaryColor: primaryColor,
cardColor: cardColor, cardColor: cardColor,
floatingActionButtonLocation: FloatingActionButtonLocation.centerTop,
), ),
); );
} }

View file

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