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
|
// 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue