OHO-25(Updated UI)
This commit is contained in:
parent
d1964c0a10
commit
394da30b82
13 changed files with 1821 additions and 934 deletions
BIN
Frontend/assets/money.png
Normal file
BIN
Frontend/assets/money.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -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,31 +167,346 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
// 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
label: const Text(
|
||||
'Sign in with Google',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onPressed: _handleGoogleSignIn,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
// 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
|
||||
? 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: const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Color(0xFF1DB584),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,215 +164,441 @@ 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,
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, -8),
|
||||
return SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Container(
|
||||
height: context.responsiveHeight(560),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(context.responsiveWidth(28)),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Top drag handle
|
||||
Container(
|
||||
width: 48,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 16, bottom: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, -8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
),
|
||||
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: accentColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(Icons.mail_outline, color: accentColor, size: 24),
|
||||
BoxShadow(
|
||||
color: primaryColor.withOpacity(0.05),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 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),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Animated Header
|
||||
FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.responsiveWidth(24),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Email Transactions',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
// Animated Icon Container
|
||||
ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: Container(
|
||||
width: context.responsiveWidth(56),
|
||||
height: context.responsiveWidth(56),
|
||||
decoration: BoxDecoration(
|
||||
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.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Forward receipts to auto-add transactions',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor.withOpacity(0.6),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
SizedBox(width: context.responsiveWidth(16)),
|
||||
|
||||
// Title Section
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Email Transactions',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor,
|
||||
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: context.responsiveText(13),
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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(
|
||||
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.refresh_rounded,
|
||||
color: accentColor,
|
||||
size: context.responsiveWidth(22),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh, color: accentColor),
|
||||
onPressed: _autoSyncEmails,
|
||||
),
|
||||
),
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _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(
|
||||
color: accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.receipt_long,
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
icon: Icons.home_outlined,
|
||||
activeIcon: Icons.home_rounded,
|
||||
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({
|
||||
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),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? activeIcon : icon,
|
||||
color: isActive ? accentColor : primaryColor.withOpacity(0.6),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
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: cardColor.withOpacity(0.65),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
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.spaceEvenly,
|
||||
children:
|
||||
_navItems
|
||||
.map((item) => _buildNavItem(context, item))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: [
|
||||
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),
|
||||
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});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
@ -65,9 +66,14 @@ class QuickActionsPage extends StatelessWidget {
|
|||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// Navigate to Statistics
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const Statistics(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
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),
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(icon, color: iconColor, size: context.responsiveText(20)),
|
||||
SizedBox(width: context.responsiveWidth(8)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: GoogleFonts.inter(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
fontSize: context.responsiveText(14),
|
||||
),
|
||||
),
|
||||
SizedBox(width: context.responsiveWidth(12)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: GoogleFonts.inter(
|
||||
fontWeight: FontWeight.w500,
|
||||
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: context.responsiveText(20),
|
||||
),
|
||||
child: Icon(icon, color: accentColor, size: 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,223 +214,206 @@ 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: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accentColor.withOpacity(0.05),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: accentColor.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: Colors.green,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Review Transaction',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Please confirm the details below',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor.withOpacity(0.6),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Transaction Details
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: primaryColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailItem(
|
||||
icon: Icons.description_outlined,
|
||||
label: 'Description',
|
||||
value: description?.toString() ?? 'No description',
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildDetailItem(
|
||||
icon: Icons.payments_outlined,
|
||||
label: 'Amount',
|
||||
value: '₹$amount',
|
||||
isAmount: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildDetailItem(
|
||||
icon: Icons.category_outlined,
|
||||
label: 'Category',
|
||||
value: category?.toString() ?? 'Uncategorized',
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildDetailItem(
|
||||
icon: Icons.calendar_today_outlined,
|
||||
label: 'Date',
|
||||
value: '${date.day}/${date.month}/${date.year}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Action Buttons
|
||||
Column(
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(context.responsiveWidth(20)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Confirm Button
|
||||
// Header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
padding: EdgeInsets.all(context.responsiveWidth(18)),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.green, Colors.green.shade600],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(18),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
color: accentColor.withOpacity(0.08),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(context.responsiveWidth(10)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(10),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: Colors.green,
|
||||
size: context.responsiveText(22),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: context.responsiveWidth(14)),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Review Transaction",
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: context.responsiveText(16),
|
||||
fontWeight: FontWeight.w700,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Please confirm the details below",
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: context.responsiveText(13),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: primaryColor.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: context.responsiveHeight(24)),
|
||||
|
||||
// Cancel Button
|
||||
// Details Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
padding: EdgeInsets.all(context.responsiveWidth(20)),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(18),
|
||||
),
|
||||
border: Border.all(
|
||||
color: primaryColor.withOpacity(0.2),
|
||||
color: primaryColor.withOpacity(0.08),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Edit or Cancel',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailItem(
|
||||
icon: Icons.description_outlined,
|
||||
label: "Description",
|
||||
value: description?.toString() ?? "No description",
|
||||
),
|
||||
SizedBox(height: context.responsiveHeight(16)),
|
||||
_buildDetailItem(
|
||||
icon: Icons.payments_outlined,
|
||||
label: "Amount",
|
||||
value: "₹$amount",
|
||||
isAmount: true,
|
||||
),
|
||||
SizedBox(height: context.responsiveHeight(16)),
|
||||
_buildDetailItem(
|
||||
icon: Icons.category_outlined,
|
||||
label: "Category",
|
||||
value: category?.toString() ?? "Uncategorized",
|
||||
),
|
||||
SizedBox(height: context.responsiveHeight(16)),
|
||||
_buildDetailItem(
|
||||
icon: Icons.calendar_today_outlined,
|
||||
label: "Date",
|
||||
value: "${date.day}/${date.month}/${date.year}",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Buttons
|
||||
Column(
|
||||
children: [
|
||||
// Confirm
|
||||
GestureDetector(
|
||||
onTapDown: (_) => _controller.forward(),
|
||||
onTapUp: (_) => _saveTransaction(context),
|
||||
child: AnimatedScale(
|
||||
scale: 1,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: context.responsiveHeight(14),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.green, Colors.green.shade700],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(14),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Confirm & Save Transaction",
|
||||
style: GoogleFonts.inter(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,335 +202,522 @@ 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
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, -12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Handle Bar
|
||||
Container(
|
||||
width: 48,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 16, bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
24,
|
||||
16,
|
||||
24,
|
||||
16,
|
||||
), // Added bottom padding
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: accentColor.withOpacity(0.2),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Icon(Icons.mic_outlined, color: accentColor, size: 28),
|
||||
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: BorderRadius.vertical(
|
||||
top: Radius.circular(context.responsiveWidth(32)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Voice Transaction',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor,
|
||||
fontSize: 20, // Reduced font size
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4), // Added spacing
|
||||
Text(
|
||||
'Speak naturally to record your transaction',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor.withOpacity(0.6),
|
||||
fontSize: 12, // Reduced font size
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: context.responsiveWidth(40),
|
||||
offset: Offset(0, -context.responsiveHeight(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Main content with SingleChildScrollView to prevent overflow
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Status Card
|
||||
// Handle Bar
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
width: context.responsiveWidth(48),
|
||||
height: context.responsiveHeight(4),
|
||||
margin: EdgeInsets.only(
|
||||
top: context.responsiveHeight(20),
|
||||
bottom: context.responsiveHeight(8),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_isListening
|
||||
? accentColor.withOpacity(0.08)
|
||||
: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color:
|
||||
_isListening
|
||||
? accentColor.withOpacity(0.2)
|
||||
: primaryColor.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
color: primaryColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
),
|
||||
|
||||
// Header
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
context.responsiveWidth(24),
|
||||
context.responsiveHeight(20),
|
||||
context.responsiveWidth(24),
|
||||
context.responsiveHeight(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Microphone Button
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _isListening ? _pulseAnimation.value : 1.0,
|
||||
child: GestureDetector(
|
||||
onTap: _processing ? null : _listen,
|
||||
child: Container(
|
||||
width: 80, // Reduced size
|
||||
height: 80, // Reduced size
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_isListening
|
||||
? accentColor.withOpacity(0.15)
|
||||
: primaryColor.withOpacity(0.05),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color:
|
||||
_isListening
|
||||
? accentColor.withOpacity(0.4)
|
||||
: primaryColor.withOpacity(0.15),
|
||||
width: 2.5,
|
||||
),
|
||||
boxShadow:
|
||||
_isListening
|
||||
? [
|
||||
BoxShadow(
|
||||
color: accentColor.withOpacity(
|
||||
0.2,
|
||||
),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child:
|
||||
_processing
|
||||
? const CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.blue,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
_isListening
|
||||
? Icons.mic
|
||||
: Icons.mic_none,
|
||||
size: 36, // Reduced size
|
||||
color:
|
||||
_isListening
|
||||
? accentColor
|
||||
: primaryColor.withOpacity(
|
||||
0.6,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: context.responsiveWidth(56),
|
||||
height: context.responsiveWidth(56),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(18),
|
||||
),
|
||||
border: Border.all(
|
||||
color: accentColor.withOpacity(0.3),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.mic_outlined,
|
||||
color: accentColor,
|
||||
size: context.responsiveWidth(28),
|
||||
),
|
||||
),
|
||||
SizedBox(width: context.responsiveWidth(16)),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Voice Transaction',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor,
|
||||
fontSize: context.responsiveText(22),
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16), // Reduced spacing
|
||||
// Status Text
|
||||
Text(
|
||||
_processing
|
||||
? 'Processing your request...'
|
||||
: _isListening
|
||||
? 'Listening... Tap to stop'
|
||||
: 'Tap microphone to start recording',
|
||||
style: GoogleFonts.inter(
|
||||
color:
|
||||
_isListening || _processing
|
||||
? accentColor
|
||||
: primaryColor.withOpacity(0.7),
|
||||
fontSize: 14, // Reduced font size
|
||||
fontWeight: FontWeight.w600,
|
||||
SizedBox(height: context.responsiveHeight(6)),
|
||||
Text(
|
||||
'Speak naturally to record your transaction',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor.withOpacity(0.65),
|
||||
fontSize: context.responsiveText(14),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20), // Reduced spacing
|
||||
// Spoken Text Display
|
||||
if (_spokenText.isNotEmpty) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16), // Reduced padding
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
16,
|
||||
), // Reduced radius
|
||||
border: Border.all(
|
||||
color: accentColor.withOpacity(0.15),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accentColor.withOpacity(0.05),
|
||||
blurRadius: 8, // Reduced blur
|
||||
offset: const Offset(0, 2), // Reduced offset
|
||||
),
|
||||
],
|
||||
// Main Content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.responsiveWidth(24),
|
||||
vertical: context.responsiveHeight(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.email, color: accentColor, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Transcript',
|
||||
style: GoogleFonts.inter(
|
||||
color: accentColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
// Voice Input Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(
|
||||
context.responsiveWidth(24),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
_isListening
|
||||
? 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.3)
|
||||
: primaryColor.withOpacity(0.08),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Microphone Button
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale:
|
||||
_isListening
|
||||
? _pulseAnimation.value
|
||||
: 1.0,
|
||||
child: GestureDetector(
|
||||
onTap: _processing ? null : _listen,
|
||||
child: Container(
|
||||
width: context.responsiveWidth(96),
|
||||
height: context.responsiveWidth(96),
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
_isListening
|
||||
? 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.2),
|
||||
width: 2.5,
|
||||
),
|
||||
boxShadow:
|
||||
_isListening
|
||||
? [
|
||||
BoxShadow(
|
||||
color: accentColor
|
||||
.withOpacity(0.3),
|
||||
blurRadius: context
|
||||
.responsiveWidth(
|
||||
24,
|
||||
),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
color: primaryColor
|
||||
.withOpacity(0.1),
|
||||
blurRadius: context
|
||||
.responsiveWidth(
|
||||
12,
|
||||
),
|
||||
offset: Offset(
|
||||
0,
|
||||
context
|
||||
.responsiveHeight(
|
||||
4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child:
|
||||
_processing
|
||||
? SizedBox(
|
||||
width: context
|
||||
.responsiveWidth(32),
|
||||
height: context
|
||||
.responsiveWidth(32),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<
|
||||
Color
|
||||
>(accentColor),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
_isListening
|
||||
? Icons.mic
|
||||
: Icons.mic_none,
|
||||
size: context
|
||||
.responsiveWidth(42),
|
||||
color:
|
||||
_isListening
|
||||
? accentColor
|
||||
: primaryColor
|
||||
.withOpacity(
|
||||
0.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: context.responsiveHeight(20)),
|
||||
|
||||
// Wave Indicator (only when listening)
|
||||
if (_isListening) ...[
|
||||
_buildWaveIndicator(),
|
||||
SizedBox(
|
||||
height: context.responsiveHeight(16),
|
||||
),
|
||||
],
|
||||
|
||||
// Status 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: context.responsiveText(16),
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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: EdgeInsets.all(
|
||||
context.responsiveWidth(20),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(20),
|
||||
),
|
||||
border: Border.all(
|
||||
color: accentColor.withOpacity(0.2),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accentColor.withOpacity(
|
||||
0.08,
|
||||
),
|
||||
blurRadius: context.responsiveWidth(
|
||||
16,
|
||||
),
|
||||
offset: Offset(
|
||||
0,
|
||||
context.responsiveHeight(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.transcribe_outlined,
|
||||
color: accentColor,
|
||||
size: context.responsiveWidth(
|
||||
18,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: context.responsiveWidth(
|
||||
8,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Transcript',
|
||||
style: GoogleFonts.inter(
|
||||
color: accentColor,
|
||||
fontSize: context
|
||||
.responsiveText(13),
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: context.responsiveHeight(
|
||||
12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_spokenText,
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor,
|
||||
fontSize: context.responsiveText(
|
||||
15,
|
||||
),
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
|
||||
SizedBox(height: context.responsiveHeight(32)),
|
||||
|
||||
// Close Button
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: context.responsiveHeight(56),
|
||||
decoration: BoxDecoration(
|
||||
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,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(16),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(
|
||||
context.responsiveWidth(16),
|
||||
),
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Close',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor.withOpacity(0.8),
|
||||
fontSize: context.responsiveText(16),
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_spokenText,
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor,
|
||||
fontSize: 14, // Reduced font size
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.4, // Reduced line height
|
||||
),
|
||||
maxLines: 5, // Limit lines
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20), // Reduced spacing
|
||||
],
|
||||
|
||||
// Action Button
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 50, // Reduced height
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12), // Reduced radius
|
||||
border: Border.all(
|
||||
color: primaryColor.withOpacity(0.15),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Close',
|
||||
style: GoogleFonts.inter(
|
||||
color: primaryColor.withOpacity(0.8),
|
||||
fontSize: 14, // Reduced font size
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: context.responsiveHeight(20)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16), // Added bottom spacing for safety
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -767,6 +767,7 @@ class StatisticsState extends State<Statistics> with TickerProviderStateMixin {
|
|||
accentColor: accentColor,
|
||||
primaryColor: primaryColor,
|
||||
cardColor: cardColor,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerTop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue