From f5dbd7eea8d0a20ab3d49482885fd358aa3a6a55 Mon Sep 17 00:00:00 2001 From: lokeshramchand-ctrl Date: Wed, 27 Aug 2025 12:23:12 +0530 Subject: [PATCH] OHO-21(Adding voice-only UI) --- .../lib/main_pages/HomePage/animated.dart | 37 + Frontend/lib/main_pages/HomePage/confirm.dart | 389 +++++++++ .../lib/main_pages/HomePage/e_dialog.dart | 119 +++ .../lib/main_pages/HomePage/greeting.dart | 105 +++ .../lib/main_pages/HomePage/homepage.dart | 815 +----------------- Frontend/lib/main_pages/HomePage/navbar.dart | 161 ++-- .../lib/main_pages/HomePage/t_dialog.dart | 117 +++ .../HomePage/transaction_button.dart | 88 ++ .../lib/main_pages/HomePage/voice_dialog.dart | 473 ++++++++++ 9 files changed, 1427 insertions(+), 877 deletions(-) create mode 100644 Frontend/lib/main_pages/HomePage/animated.dart create mode 100644 Frontend/lib/main_pages/HomePage/confirm.dart create mode 100644 Frontend/lib/main_pages/HomePage/e_dialog.dart create mode 100644 Frontend/lib/main_pages/HomePage/greeting.dart create mode 100644 Frontend/lib/main_pages/HomePage/t_dialog.dart create mode 100644 Frontend/lib/main_pages/HomePage/transaction_button.dart create mode 100644 Frontend/lib/main_pages/HomePage/voice_dialog.dart diff --git a/Frontend/lib/main_pages/HomePage/animated.dart b/Frontend/lib/main_pages/HomePage/animated.dart new file mode 100644 index 0000000..d8d8999 --- /dev/null +++ b/Frontend/lib/main_pages/HomePage/animated.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class AnimatedSection extends StatelessWidget { + final int delay; + final Widget child; + + const AnimatedSection({ + super.key, + required this.delay, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: Future.delayed(Duration(milliseconds: delay)), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Opacity(opacity: 0, child: child); + } + return AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 600), + curve: Curves.easeOut, + child: AnimatedSlide( + offset: snapshot.connectionState == ConnectionState.done + ? Offset.zero + : const Offset(0, 0.1), + duration: const Duration(milliseconds: 600), + curve: Curves.easeOutCubic, + child: child, + ), + ); + }, + ); + } +} diff --git a/Frontend/lib/main_pages/HomePage/confirm.dart b/Frontend/lib/main_pages/HomePage/confirm.dart new file mode 100644 index 0000000..34f9c83 --- /dev/null +++ b/Frontend/lib/main_pages/HomePage/confirm.dart @@ -0,0 +1,389 @@ +// ignore_for_file: use_build_context_synchronously, deprecated_member_use + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'package:monarch/other_pages/colors.dart'; +import 'package:monarch/other_pages/enviroment.dart'; + +class ConfirmTransactionPage extends StatelessWidget { + final Map data; + + const ConfirmTransactionPage({super.key, required this.data}); + + Future _saveTransaction(BuildContext context) async { + const String saveUrl = '${Environment.baseUrl}/api/transaction/add'; + + try { + final response = await http.post( + Uri.parse(saveUrl), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'description': data['description'], + 'amount': data['amount'], + 'category': data['category'], + 'date': data['date'] ?? DateTime.now().toIso8601String(), + }), + ); + + if (response.statusCode == 200) { + _showCustomSnackBar( + context: context, + message: '✅ Transaction saved successfully!', + icon: Icons.check_circle, + backgroundColor: const Color(0xFF4CAF50), + iconColor: Colors.white, + duration: const Duration(seconds: 3), + ); + Navigator.popUntil(context, (route) => route.isFirst); + } else { + throw Exception('Save failed with status: ${response.statusCode}'); + } + } catch (e) { + _showCustomSnackBar( + context: context, + message: 'Error saving transaction: ${e.toString()}', + icon: Icons.error_outline, + backgroundColor: const Color(0xFFFF6B6B), + iconColor: Colors.white, + duration: const Duration(seconds: 4), + ); + } + } + + void _showCustomSnackBar({ + required BuildContext context, + required String message, + required IconData icon, + required Color backgroundColor, + required Color iconColor, + required Duration duration, + }) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(icon, color: iconColor), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + backgroundColor: backgroundColor, + duration: duration, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: const EdgeInsets.all(16), + ), + ); + } + + Widget _buildDetailItem({ + required IconData icon, + required String label, + required String value, + bool isAmount = false, + }) { + return Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: accentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: accentColor, size: 20), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.6), + fontSize: 12, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: GoogleFonts.inter( + color: isAmount ? Colors.green : primaryColor, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final amount = data['amount']; + final description = data['description']; + final category = data['category']; + final date = + data['date'] != null + ? DateTime.parse(data['date']).toLocal() + : DateTime.now(); + + return Scaffold( + backgroundColor: backgroundColor, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: Container( + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: primaryColor.withOpacity(0.1), width: 1), + ), + child: IconButton( + icon: Icon(Icons.arrow_back, color: primaryColor), + onPressed: () => Navigator.pop(context), + ), + ), + title: Text( + 'Confirm Transaction', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: accentColor.withOpacity(0.05), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + border: Border.all( + color: accentColor.withOpacity(0.1), + width: 1.5, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.green.withOpacity(0.2), + width: 1.5, + ), + ), + child: const Icon( + Icons.check_circle_outline, + color: Colors.green, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Review Transaction', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Please confirm the details below', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.6), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Transaction Details + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: primaryColor.withOpacity(0.1), + width: 1, + ), + ), + child: Column( + children: [ + _buildDetailItem( + icon: Icons.description_outlined, + label: 'Description', + value: description?.toString() ?? 'No description', + ), + + const SizedBox(height: 20), + + _buildDetailItem( + icon: Icons.payments_outlined, + label: 'Amount', + value: '₹$amount', + isAmount: true, + ), + + const SizedBox(height: 20), + + _buildDetailItem( + icon: Icons.category_outlined, + label: 'Category', + value: category?.toString() ?? 'Uncategorized', + ), + + const SizedBox(height: 20), + + _buildDetailItem( + icon: Icons.calendar_today_outlined, + label: 'Date', + value: '${date.day}/${date.month}/${date.year}', + ), + ], + ), + ), + + const Spacer(), + + // Action Buttons + Column( + children: [ + // Confirm Button + Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.green, Colors.green.shade600], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.green.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => _saveTransaction(context), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.check, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Confirm & Save Transaction', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ), + ), + + const SizedBox(height: 16), + + // Cancel Button + Container( + width: double.infinity, + height: 48, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: primaryColor.withOpacity(0.2), + width: 1, + ), + ), + child: TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Edit or Cancel', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.7), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/Frontend/lib/main_pages/HomePage/e_dialog.dart b/Frontend/lib/main_pages/HomePage/e_dialog.dart new file mode 100644 index 0000000..a65e607 --- /dev/null +++ b/Frontend/lib/main_pages/HomePage/e_dialog.dart @@ -0,0 +1,119 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:monarch/other_pages/colors.dart'; + +class EmailTransactionDialog extends StatelessWidget { + const EmailTransactionDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: BoxDecoration( + color: cardColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(28), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 32, + offset: const Offset(0, -8), + ), + ], + ), + child: Column( + children: [ + Container( + width: 48, + height: 4, + margin: const EdgeInsets.only(top: 16, bottom: 24), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(2), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: accentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: accentColor.withOpacity(0.2), + width: 1, + ), + ), + child: Icon( + Icons.mail_outline, + color: accentColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Email Transaction', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Forward receipts to add transactions', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.6), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 32), + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.email, + size: 64, + color: accentColor.withOpacity(0.3), + ), + const SizedBox(height: 16), + Text( + 'Email integration coming soon!', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.6), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/Frontend/lib/main_pages/HomePage/greeting.dart b/Frontend/lib/main_pages/HomePage/greeting.dart new file mode 100644 index 0000000..b8712d2 --- /dev/null +++ b/Frontend/lib/main_pages/HomePage/greeting.dart @@ -0,0 +1,105 @@ +// ignore_for_file: use_super_parameters, deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:monarch/main_pages/HomePage/transaction_button.dart'; +import 'package:monarch/other_pages/colors.dart'; + +class GreetingHeader extends StatelessWidget { + final String greetingText; + final Animation scaleAnimation; + final VoidCallback onEmailPressed; + final VoidCallback onVoicePressed; + + const GreetingHeader({ + Key? key, + required this.greetingText, + required this.scaleAnimation, + required this.onEmailPressed, + required this.onVoicePressed, + }) : super(key: key); + + String _getFormattedDate() { + final now = DateTime.now(); + final weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + final months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', + ]; + + return '${weekdays[now.weekday - 1]}, ${now.day} ${months[now.month - 1]}'; + } + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 28), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: AnimatedBuilder( + animation: scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: scaleAnimation.value, + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '$greetingText ', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 28, + fontWeight: FontWeight.w800, + height: 1.1, + letterSpacing: -0.5, + ), + ), + ], + ), + const SizedBox(height: 6), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: accentColor.withOpacity(0.08), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: accentColor.withOpacity(0.15), + width: 1, + ), + ), + child: Text( + _getFormattedDate(), + style: GoogleFonts.inter( + color: accentColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + }, + ), + ), + const SizedBox(width: 16), + TransactionButtons( + scaleAnimation: scaleAnimation, + onEmailPressed: onEmailPressed, + onVoicePressed: onVoicePressed, + ), + ], + ), + ); + } +} diff --git a/Frontend/lib/main_pages/HomePage/homepage.dart b/Frontend/lib/main_pages/HomePage/homepage.dart index 54e7c83..23f68da 100644 --- a/Frontend/lib/main_pages/HomePage/homepage.dart +++ b/Frontend/lib/main_pages/HomePage/homepage.dart @@ -1,19 +1,19 @@ -// ignore_for_file: deprecated_member_use, unused_local_variable, use_super_parameters, avoid_print, use_build_context_synchronously -import 'dart:convert'; +// ignore_for_file: deprecated_member_use + import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; +import 'package:monarch/main_pages/HomePage/animated.dart'; +import 'package:monarch/main_pages/HomePage/greeting.dart'; import 'package:monarch/main_pages/HomePage/navbar.dart'; +import 'package:monarch/main_pages/HomePage/t_dialog.dart'; +import 'package:monarch/main_pages/HomePage/voice_dialog.dart'; import 'package:monarch/other_pages/colors.dart'; import 'package:monarch/support/fetch_service.dart'; import 'package:monarch/main_pages/HomePage/hero_card.dart'; import 'package:monarch/main_pages/HomePage/quick_actions.dart'; import 'package:monarch/support/transcations_recent.dart'; -import 'package:http/http.dart' as http; -import 'package:speech_to_text/speech_to_text.dart' as stt; -import 'package:monarch/other_pages/enviroment.dart'; class FinTrackHomePage extends StatefulWidget { - const FinTrackHomePage({Key? key}) : super(key: key); + const FinTrackHomePage({super.key}); @override State createState() => _FinTrackHomePageState(); @@ -21,20 +21,15 @@ class FinTrackHomePage extends StatefulWidget { class _FinTrackHomePageState extends State with TickerProviderStateMixin { - // Animation Controllers late AnimationController _fadeController; late AnimationController _slideController; late AnimationController _scaleController; - // Animations late Animation _fadeAnimation; late Animation _slideAnimation; late Animation _scaleAnimation; - // Navigation State int _selectedIndex = 0; - - // Time-based greeting data String _greetingText = ''; @override @@ -53,14 +48,12 @@ class _FinTrackHomePageState extends State super.dispose(); } - // Initialize data fetching void _initializeData() { fetchRecentTransactions().then((transactions) { // Handle transactions data }); } - // Update greeting based on current time void _updateGreeting() { final now = DateTime.now(); final hour = now.hour; @@ -76,7 +69,6 @@ class _FinTrackHomePageState extends State } } - // Setup enhanced animation controllers void _setupAnimations() { _fadeController = AnimationController( duration: const Duration(milliseconds: 1500), @@ -108,7 +100,6 @@ class _FinTrackHomePageState extends State CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut), ); - // Staggered animation start _fadeController.forward(); Future.delayed(const Duration(milliseconds: 200), () { _slideController.forward(); @@ -118,215 +109,6 @@ class _FinTrackHomePageState extends State }); } - // Enhanced greeting header with time-based content - Widget _buildGreetingHeader() { - return Container( - margin: const EdgeInsets.only(bottom: 28), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Enhanced Greeting Section - Expanded( - child: AnimatedBuilder( - animation: _scaleAnimation, - builder: (context, child) { - return Transform.scale( - scale: _scaleAnimation.value, - alignment: Alignment.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Main greeting with emoji - Row( - children: [ - Text( - '$_greetingText ', - style: GoogleFonts.inter( - color: primaryColor, - fontSize: 32, - fontWeight: FontWeight.w800, - height: 1.1, - letterSpacing: -0.5, - ), - ), - ], - ), - const SizedBox(height: 6), - - // Enhanced sub-greeting - const SizedBox(height: 8), - // Date indicator - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: accentColor.withOpacity(0.08), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: accentColor.withOpacity(0.15), - width: 1, - ), - ), - child: Text( - _getFormattedDate(), - style: GoogleFonts.inter( - color: accentColor, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ); - }, - ), - ), - const SizedBox(width: 16), - // Enhanced notification button - _buildTransactionButtons(), - ], - ), - ); - } - - // Get formatted date string - String _getFormattedDate() { - final now = DateTime.now(); - final weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - final months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; - - return '${weekdays[now.weekday - 1]}, ${now.day} ${months[now.month - 1]}'; - } - - // Enhanced Transaction Entry Buttons with perfect UX - Widget _buildTransactionButtons() { - return AnimatedBuilder( - animation: _scaleAnimation, - builder: (context, child) { - return Transform.scale( - scale: _scaleAnimation.value, - child: Row( - children: [ - // Gmail Transaction Button - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: accentColor.withOpacity(0.08), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 0, - ), - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - border: Border.all( - color: accentColor.withOpacity(0.08), - width: 1.5, - ), - ), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(20), - child: InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () { - _showTransactionEntryDialog('email'); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - ), - child: Icon( - Icons.mail_outline, - color: accentColor, - size: 28, - ), - ), - ), - ), - ), - - const SizedBox(width: 12), - - // Voice Transaction Button - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: accentColor.withOpacity(0.08), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 0, - ), - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - border: Border.all( - color: accentColor.withOpacity(0.08), - width: 1.5, - ), - ), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(20), - child: InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () { - _showTransactionEntryDialog('voice'); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - ), - child: Icon( - Icons.mic_outlined, - color: accentColor, - size: 28, - ), - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - // Transaction Entry Dialog with voice functionality void _showTransactionEntryDialog(String type) { if (type == 'voice') { showModalBottomSheet( @@ -336,154 +118,34 @@ class _FinTrackHomePageState extends State builder: (context) => VoiceTransactionDialog(), ); } else { - // Email dialog (unchanged) showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, - builder: - (context) => Container( - height: MediaQuery.of(context).size.height * 0.7, - decoration: BoxDecoration( - color: cardColor, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(28), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 32, - offset: const Offset(0, -8), - ), - ], - ), - child: Column( - children: [ - // Handle Bar - Container( - width: 48, - height: 4, - margin: const EdgeInsets.only(top: 16, bottom: 24), - decoration: BoxDecoration( - color: primaryColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(2), - ), - ), - - // Dialog Content - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: accentColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: accentColor.withOpacity(0.2), - width: 1, - ), - ), - child: Icon( - Icons.mail_outline, - color: accentColor, - size: 24, - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Email Transaction', - style: GoogleFonts.inter( - color: primaryColor, - fontSize: 20, - fontWeight: FontWeight.w700, - ), - ), - Text( - 'Forward receipts to add transactions', - style: GoogleFonts.inter( - color: primaryColor.withOpacity(0.6), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 32), - - // Placeholder content - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.email, - size: 64, - color: accentColor.withOpacity(0.3), - ), - const SizedBox(height: 16), - Text( - 'Email integration coming soon!', - style: GoogleFonts.inter( - color: primaryColor.withOpacity(0.6), - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), + builder: (context) => EmailTransactionDialog(), ); } } - // Enhanced main content with better spacing Widget _buildMainContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Balance Card Section with staggered animation - _buildAnimatedSection( + AnimatedSection( delay: 600, child: Container( margin: const EdgeInsets.only(bottom: 32), child: const BalanceCardPage(), ), ), - - // Quick Actions Section with staggered animation - _buildAnimatedSection( + AnimatedSection( delay: 800, child: Container( margin: const EdgeInsets.only(bottom: 32), child: QuickActionsPage(), ), ), - - // Recent Transactions Section with staggered animation - _buildAnimatedSection( + AnimatedSection( delay: 1000, child: Container( margin: const EdgeInsets.only(bottom: 32), @@ -492,39 +154,11 @@ class _FinTrackHomePageState extends State ), ), ), - - // Bottom spacing for FAB and navigation const SizedBox(height: 120), ], ); } - // Helper method for staggered animations - Widget _buildAnimatedSection({required int delay, required Widget child}) { - return FutureBuilder( - future: Future.delayed(Duration(milliseconds: delay)), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Opacity(opacity: 0, child: child); - } - return AnimatedOpacity( - opacity: 1, - duration: const Duration(milliseconds: 600), - curve: Curves.easeOut, - child: AnimatedSlide( - offset: - snapshot.connectionState == ConnectionState.done - ? Offset.zero - : const Offset(0, 0.1), - duration: const Duration(milliseconds: 600), - curve: Curves.easeOutCubic, - child: child, - ), - ); - }, - ); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -536,7 +170,6 @@ class _FinTrackHomePageState extends State position: _slideAnimation, child: RefreshIndicator( onRefresh: () async { - // Add refresh functionality _initializeData(); _updateGreeting(); setState(() {}); @@ -564,11 +197,23 @@ class _FinTrackHomePageState extends State ), ), child: Padding( - padding: const EdgeInsets.fromLTRB(24, 20, 24, 0), + padding: const EdgeInsets.fromLTRB( + 24, + 20, + 24, + 80, + ), // Added bottom padding for navbar child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildGreetingHeader(), + GreetingHeader( + greetingText: _greetingText, + scaleAnimation: _scaleAnimation, + onEmailPressed: + () => _showTransactionEntryDialog('email'), + onVoicePressed: + () => _showTransactionEntryDialog('voice'), + ), _buildMainContent(), ], ), @@ -581,7 +226,8 @@ class _FinTrackHomePageState extends State ), ), ), - floatingActionButton: CustomNavBar( + // Use bottomNavigationBar instead of floatingActionButton + bottomNavigationBar: CustomNavBar( currentIndex: _selectedIndex, onTap: (index) { setState(() => _selectedIndex = index); @@ -594,410 +240,3 @@ class _FinTrackHomePageState extends State ); } } - -// Voice Transaction Dialog Widget -class VoiceTransactionDialog extends StatefulWidget { - const VoiceTransactionDialog({super.key}); - - @override - _VoiceTransactionDialogState createState() => _VoiceTransactionDialogState(); -} - -class _VoiceTransactionDialogState extends State - with SingleTickerProviderStateMixin { - late stt.SpeechToText _speech; - bool _isListening = false; - String _spokenText = ''; - bool _processing = false; - late AnimationController _animationController; - late Animation _scaleAnimation; - late Animation _opacityAnimation; - - @override - void initState() { - super.initState(); - _speech = stt.SpeechToText(); - - // Animation setup - _animationController = AnimationController( - vsync: this, - duration: Duration(milliseconds: 800), - ); - - _scaleAnimation = TweenSequence([ - TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.2), weight: 1), - TweenSequenceItem(tween: Tween(begin: 1.2, end: 1.0), weight: 1), - ]).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), - ); - - _opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeIn), - ); - - _animationController.repeat(reverse: true); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - void _listen() async { - if (!_isListening) { - bool available = await _speech.initialize( - onStatus: (val) async { - print('Status: $val'); - if (val == 'done') { - setState(() { - _isListening = false; - _processing = true; - }); - await _speech.stop(); - if (_spokenText.isNotEmpty) { - await _sendToBackend(); - } - setState(() { - _processing = false; - }); - } - }, - onError: (val) => print('Error: $val'), - ); - - if (available) { - setState(() { - _isListening = true; - _spokenText = ''; - }); - _speech.listen( - onResult: (val) { - setState(() { - _spokenText = val.recognizedWords; - }); - }, - ); - } - } else { - setState(() { - _isListening = false; - _processing = true; - }); - _speech.stop(); - } - } - - Future _sendToBackend() async { - try { - final response = await http.post( - Uri.parse('${Environment.baseUrl}/api/transactions/voice'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({'voiceInput': _spokenText}), - ); - - if (response.statusCode == 200) { - final Map responseData = jsonDecode(response.body); - final transactionData = responseData['data']; - - // Close the bottom sheet - Navigator.pop(context); - - // Show confirmation page - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ConfirmTransactionPage(data: transactionData), - ), - ); - } else { - throw Exception('❌ Failed to process transaction'); - } - } catch (e) { - print(e); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); - setState(() { - _processing = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final cardColor = Theme.of(context).cardColor; - final primaryColor = Theme.of(context).primaryColor; - final accentColor = Theme.of(context).colorScheme.secondary; - - return Container( - height: MediaQuery.of(context).size.height * 0.7, - decoration: BoxDecoration( - color: cardColor, - borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 32, - offset: const Offset(0, -8), - ), - ], - ), - child: Column( - children: [ - // Handle Bar - Container( - width: 48, - height: 4, - margin: const EdgeInsets.only(top: 16, bottom: 24), - decoration: BoxDecoration( - color: primaryColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(2), - ), - ), - - // Dialog Content - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: accentColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: accentColor.withOpacity(0.2), - width: 1, - ), - ), - child: Icon( - Icons.mic_outlined, - color: accentColor, - size: 24, - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Voice Transaction', - style: GoogleFonts.inter( - color: primaryColor, - fontSize: 20, - fontWeight: FontWeight.w700, - ), - ), - Text( - 'Speak to record your transaction', - style: GoogleFonts.inter( - color: primaryColor.withOpacity(0.6), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 32), - - // Voice input content - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_processing) - Column( - children: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - accentColor, - ), - ), - const SizedBox(height: 16), - Text( - 'Processing your request...', - style: GoogleFonts.inter( - color: primaryColor.withOpacity(0.6), - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ], - ) - else - Column( - children: [ - // Animated microphone button - ScaleTransition( - scale: - _isListening - ? _scaleAnimation - : AlwaysStoppedAnimation(1.0), - child: Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: - _isListening - ? accentColor.withOpacity(0.2) - : accentColor.withOpacity(0.1), - shape: BoxShape.circle, - border: Border.all( - color: - _isListening - ? accentColor - : accentColor.withOpacity(0.3), - width: 2, - ), - ), - child: IconButton( - icon: Icon( - _isListening ? Icons.mic : Icons.mic_none, - size: 40, - color: - _isListening - ? accentColor - : primaryColor.withOpacity(0.6), - ), - onPressed: _listen, - ), - ), - ), - const SizedBox(height: 24), - - // Status text - Text( - _isListening - ? 'Listening...' - : 'Tap the microphone to start', - style: GoogleFonts.inter( - color: primaryColor.withOpacity(0.6), - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - - const SizedBox(height: 16), - - // Speech text with smooth appearance - AnimatedOpacity( - opacity: _spokenText.isNotEmpty ? 1.0 : 0.0, - duration: Duration(milliseconds: 300), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: primaryColor.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _spokenText, - style: GoogleFonts.inter( - color: primaryColor, - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} - -// Confirm Transaction Page -class ConfirmTransactionPage extends StatelessWidget { - final Map data; - - const ConfirmTransactionPage({super.key, required this.data}); - - Future _saveTransaction(BuildContext context) async { - const String saveUrl = '${Environment.baseUrl}/api/transaction/add'; - - try { - final response = await http.post( - Uri.parse(saveUrl), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'description': data['description'], - 'amount': data['amount'], - }), - ); - - if (response.statusCode == 200) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('✅ Transaction Saved!'))); - Navigator.popUntil(context, (route) => route.isFirst); - } else { - throw Exception('❌ Save failed'); - } - } catch (e) { - print(e); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error: $e'))); - } - } - - @override - Widget build(BuildContext context) { - final amount = data['amount']; - final description = data['description']; - final category = data['category']; - - return Scaffold( - appBar: AppBar( - title: const Text('Confirm Transaction'), - backgroundColor: Colors.transparent, - elevation: 0, - ), - body: Padding( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - Text( - "💬 Description: $description", - style: const TextStyle(fontSize: 18), - ), - Text("💰 Amount: ₹$amount", style: const TextStyle(fontSize: 18)), - Text( - "📂 Category: $category", - style: const TextStyle(fontSize: 18), - ), - const SizedBox(height: 30), - ElevatedButton.icon( - icon: const Icon(Icons.check), - label: const Text('Confirm & Save'), - onPressed: () => _saveTransaction(context), - ), - const SizedBox(height: 10), - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Edit or Cancel'), - ), - ], - ), - ), - ); - } -} diff --git a/Frontend/lib/main_pages/HomePage/navbar.dart b/Frontend/lib/main_pages/HomePage/navbar.dart index ea0df5c..bd3b670 100644 --- a/Frontend/lib/main_pages/HomePage/navbar.dart +++ b/Frontend/lib/main_pages/HomePage/navbar.dart @@ -1,14 +1,11 @@ // ignore_for_file: deprecated_member_use -import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:monarch/support/add.dart'; -import 'package:monarch/main_pages/HomePage/homepage.dart'; -import 'package:monarch/main_pages/Statistics/statistics.dart'; +import 'package:monarch/other_pages/colors.dart'; class CustomNavBar extends StatelessWidget { final int currentIndex; - final void Function(int) onTap; + final Function(int) onTap; final Color backgroundColor; final Color accentColor; final Color primaryColor; @@ -24,100 +21,86 @@ class CustomNavBar extends StatelessWidget { required this.cardColor, }); - // Navigation items - final List _navItems = const [ - NavItem(icon: Icons.home_rounded, index: 0, page: FinTrackHomePage()), - NavItem(icon: Icons.bar_chart_rounded, index: 1, page: Statistics()), - NavItem(icon: Icons.settings_rounded, index: 2, page: AddExpenseScreen()), - ]; - @override Widget build(BuildContext context) { - return Align( - alignment: Alignment.bottomCenter, - child: Container( - margin: const EdgeInsets.only(bottom: 20), - padding: const EdgeInsets.symmetric(horizontal: 20), - height: 72, - width: MediaQuery.of(context).size.width * 0.7, // centered & responsive - decoration: BoxDecoration( - color: cardColor.withOpacity(0.65), - borderRadius: BorderRadius.circular(40), - boxShadow: [ - BoxShadow( - color: primaryColor.withOpacity(0.15), - blurRadius: 30, - offset: const Offset(0, 8), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(40), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: - _navItems - .map((item) => _buildNavItem(context, item)) - .toList(), - ), + return Container( + height: 80, // Fixed height for bottom navigation + decoration: BoxDecoration( + color: backgroundColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, -2), ), - ), + ], ), - ); - } - - Widget _buildNavItem(BuildContext context, NavItem item) { - final isActive = currentIndex == item.index; - return GestureDetector( - onTap: () { - onTap(item.index); - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => item.page), - ); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 250), - width: isActive ? 38 : 30, - height: isActive ? 38 : 30, - decoration: BoxDecoration( - color: - isActive - ? primaryColor.withOpacity(0.15) - : Colors.transparent, - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - item.icon, - color: isActive ? primaryColor : primaryColor.withOpacity(0.4), - size: 24, - ), + _buildNavItem( + icon: Icons.home_outlined, + activeIcon: Icons.home_rounded, + label: 'Home', + index: 0, ), - const SizedBox(height: 4), - AnimatedContainer( - duration: const Duration(milliseconds: 250), - width: 6, - height: 6, - decoration: BoxDecoration( - color: isActive ? accentColor : Colors.transparent, - shape: BoxShape.circle, - ), + _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, ), ], ), ); } -} -// 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}); + Widget _buildNavItem({ + required IconData icon, + required IconData activeIcon, + required String label, + required int index, + }) { + final isActive = currentIndex == index; + + return Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onTap(index), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isActive ? activeIcon : icon, + color: isActive ? accentColor : primaryColor.withOpacity(0.6), + size: 24, + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + color: isActive ? accentColor : primaryColor.withOpacity(0.6), + fontSize: 12, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, + ), + ), + ], + ), + ), + ), + ); + } } diff --git a/Frontend/lib/main_pages/HomePage/t_dialog.dart b/Frontend/lib/main_pages/HomePage/t_dialog.dart new file mode 100644 index 0000000..8e22d04 --- /dev/null +++ b/Frontend/lib/main_pages/HomePage/t_dialog.dart @@ -0,0 +1,117 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:monarch/other_pages/colors.dart'; + +class EmailTransactionDialog extends StatelessWidget { + const EmailTransactionDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: BoxDecoration( + color: cardColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 32, + offset: const Offset(0, -8), + ), + ], + ), + child: Column( + children: [ + Container( + width: 48, + height: 4, + margin: const EdgeInsets.only(top: 16, bottom: 24), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(2), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: accentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: accentColor.withOpacity(0.2), + width: 1, + ), + ), + child: Icon( + Icons.mail_outline, + color: accentColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Email Transaction', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Forward receipts to add transactions', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.6), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 32), + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.email, + size: 64, + color: accentColor.withOpacity(0.3), + ), + const SizedBox(height: 16), + Text( + 'Email integration coming soon!', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.6), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/Frontend/lib/main_pages/HomePage/transaction_button.dart b/Frontend/lib/main_pages/HomePage/transaction_button.dart new file mode 100644 index 0000000..3f31378 --- /dev/null +++ b/Frontend/lib/main_pages/HomePage/transaction_button.dart @@ -0,0 +1,88 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:monarch/other_pages/colors.dart'; + +class TransactionButtons extends StatelessWidget { + final Animation scaleAnimation; + final VoidCallback onEmailPressed; + final VoidCallback onVoicePressed; + + const TransactionButtons({ + super.key, + required this.scaleAnimation, + required this.onEmailPressed, + required this.onVoicePressed, + }); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: scaleAnimation.value, + child: Row( + children: [ + _buildButton( + icon: Icons.mail_outline, + onPressed: onEmailPressed, + ), + const SizedBox(width: 12), + _buildButton( + icon: Icons.mic_outlined, + onPressed: onVoicePressed, + ), + ], + ), + ); + }, + ); + } + + Widget _buildButton({required IconData icon, required VoidCallback onPressed}) { + return Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: accentColor.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 0, + ), + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: accentColor.withOpacity(0.08), + width: 1.5, + ), + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: onPressed, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + icon, + color: accentColor, + size: 28, + ), + ), + ), + ), + ); + } +} diff --git a/Frontend/lib/main_pages/HomePage/voice_dialog.dart b/Frontend/lib/main_pages/HomePage/voice_dialog.dart new file mode 100644 index 0000000..ceaa975 --- /dev/null +++ b/Frontend/lib/main_pages/HomePage/voice_dialog.dart @@ -0,0 +1,473 @@ +// ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api, use_build_context_synchronously, deprecated_member_use + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'package:monarch/speech.dart'; +import 'package:speech_to_text/speech_to_text.dart' as stt; +import 'package:monarch/other_pages/colors.dart'; +import 'package:monarch/other_pages/enviroment.dart'; + +class VoiceTransactionDialog extends StatefulWidget { + const VoiceTransactionDialog({super.key}); + + @override + _VoiceTransactionDialogState createState() => _VoiceTransactionDialogState(); +} + +class _VoiceTransactionDialogState extends State + with SingleTickerProviderStateMixin { + late stt.SpeechToText _speech; + bool _isListening = false; + String _spokenText = ''; + bool _processing = false; + late AnimationController _pulseController; + late Animation _pulseAnimation; + + @override + void initState() { + super.initState(); + _speech = stt.SpeechToText(); + + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + + _pulseAnimation = Tween(begin: 1.0, end: 1.15).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + + void _listen() async { + if (!_isListening) { + bool available = await _speech.initialize( + onStatus: (val) async { + if (val == 'done') { + setState(() { + _isListening = false; + _processing = true; + }); + _pulseController.stop(); + await _speech.stop(); + if (_spokenText.isNotEmpty) { + await _sendToBackend(); + } + setState(() { + _processing = false; + }); + } + }, + onError: (val) { + _showCustomSnackBar( + message: 'Speech recognition error: $val', + icon: Icons.mic_off, + backgroundColor: const Color(0xFFFF6B6B), + iconColor: Colors.white, + duration: const Duration(seconds: 3), + ); + }, + ); + + if (available) { + setState(() { + _isListening = true; + _spokenText = ''; + }); + _pulseController.repeat(reverse: true); + + _speech.listen( + onResult: (val) { + setState(() { + _spokenText = val.recognizedWords; + }); + }, + ); + } + } else { + setState(() { + _isListening = false; + _processing = true; + }); + _pulseController.stop(); + _speech.stop(); + } + } + + Future _sendToBackend() async { + try { + final response = await http.post( + Uri.parse('${Environment.baseUrl}/api/transactions/voice'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'voiceInput': _spokenText}), + ); + + if (response.statusCode == 200) { + final Map responseData = jsonDecode(response.body); + final transactionData = responseData['data']; + + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ConfirmTransactionPage(data: transactionData), + ), + ); + + _showCustomSnackBar( + message: 'Transaction processed successfully!', + icon: Icons.check_circle, + backgroundColor: const Color(0xFF4CAF50), + iconColor: Colors.white, + duration: const Duration(seconds: 2), + ); + } else { + throw Exception('Failed to process transaction'); + } + } catch (e) { + _showCustomSnackBar( + message: 'Error processing transaction: ${e.toString()}', + icon: Icons.error_outline, + backgroundColor: const Color(0xFFFF6B6B), + iconColor: Colors.white, + duration: const Duration(seconds: 4), + ); + setState(() { + _processing = false; + }); + } + } + + void _showCustomSnackBar({ + required String message, + required IconData icon, + required Color backgroundColor, + required Color iconColor, + required Duration duration, + }) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(icon, color: iconColor), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + backgroundColor: backgroundColor, + duration: duration, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: const EdgeInsets.all(16), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height * 0.85, // Reduced height + decoration: BoxDecoration( + color: cardColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 32, + offset: const Offset(0, -12), + ), + ], + ), + child: Column( + children: [ + // Handle Bar + Container( + width: 48, + height: 4, + margin: const EdgeInsets.only(top: 16, bottom: 8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(2), + ), + ), + + // Header + Padding( + padding: const EdgeInsets.fromLTRB( + 24, + 16, + 24, + 16, + ), // Added bottom padding + child: Row( + children: [ + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + color: accentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: accentColor.withOpacity(0.2), + width: 1.5, + ), + ), + child: Icon(Icons.mic_outlined, color: accentColor, size: 28), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Voice Transaction', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 20, // Reduced font size + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), // Added spacing + Text( + 'Speak naturally to record your transaction', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.6), + fontSize: 12, // Reduced font size + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + + // Main content with SingleChildScrollView to prevent overflow + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Status Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: + _isListening + ? accentColor.withOpacity(0.08) + : backgroundColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: + _isListening + ? accentColor.withOpacity(0.2) + : primaryColor.withOpacity(0.1), + width: 1.5, + ), + ), + child: Column( + children: [ + // Microphone Button + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Transform.scale( + scale: _isListening ? _pulseAnimation.value : 1.0, + child: GestureDetector( + onTap: _processing ? null : _listen, + child: Container( + width: 80, // Reduced size + height: 80, // Reduced size + decoration: BoxDecoration( + color: + _isListening + ? accentColor.withOpacity(0.15) + : primaryColor.withOpacity(0.05), + shape: BoxShape.circle, + border: Border.all( + color: + _isListening + ? accentColor.withOpacity(0.4) + : primaryColor.withOpacity(0.15), + width: 2.5, + ), + boxShadow: + _isListening + ? [ + BoxShadow( + color: accentColor.withOpacity( + 0.2, + ), + blurRadius: 20, + spreadRadius: 0, + ), + ] + : null, + ), + child: + _processing + ? const CircularProgressIndicator( + strokeWidth: 3, + valueColor: + AlwaysStoppedAnimation( + Colors.blue, + ), + ) + : Icon( + _isListening + ? Icons.mic + : Icons.mic_none, + size: 36, // Reduced size + color: + _isListening + ? accentColor + : primaryColor.withOpacity( + 0.6, + ), + ), + ), + ), + ); + }, + ), + const SizedBox(height: 16), // Reduced spacing + // Status Text + Text( + _processing + ? 'Processing your request...' + : _isListening + ? 'Listening... Tap to stop' + : 'Tap microphone to start recording', + style: GoogleFonts.inter( + color: + _isListening || _processing + ? accentColor + : primaryColor.withOpacity(0.7), + fontSize: 14, // Reduced font size + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + + const SizedBox(height: 20), // Reduced spacing + // Spoken Text Display + if (_spokenText.isNotEmpty) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), // Reduced padding + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular( + 16, + ), // Reduced radius + border: Border.all( + color: accentColor.withOpacity(0.15), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: accentColor.withOpacity(0.05), + blurRadius: 8, // Reduced blur + offset: const Offset(0, 2), // Reduced offset + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.email, color: accentColor, size: 16), + const SizedBox(width: 8), + Text( + 'Transcript', + style: GoogleFonts.inter( + color: accentColor, + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + _spokenText, + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 14, // Reduced font size + fontWeight: FontWeight.w500, + height: 1.4, // Reduced line height + ), + maxLines: 5, // Limit lines + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(height: 20), // Reduced spacing + ], + + // Action Button + Container( + width: double.infinity, + height: 50, // Reduced height + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), // Reduced radius + border: Border.all( + color: primaryColor.withOpacity(0.15), + width: 1, + ), + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => Navigator.pop(context), + child: Center( + child: Text( + 'Close', + style: GoogleFonts.inter( + color: primaryColor.withOpacity(0.8), + fontSize: 14, // Reduced font size + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + + const SizedBox(height: 16), // Added bottom spacing for safety + ], + ), + ), + ), + ], + ), + ); + } +}