refactor: split main.dart into structured directories
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'screens/expenses_screen.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
bool _isDarkMode = false;
|
||||
|
||||
void toggleTheme() {
|
||||
setState(() {
|
||||
_isDarkMode = !_isDarkMode;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Expenses Tracker',
|
||||
debugShowCheckedModeBanner: false,
|
||||
themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
home: ExpensesScreen(toggleTheme: toggleTheme, isDarkMode: _isDarkMode),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1210
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Category {
|
||||
final String name;
|
||||
final double amount;
|
||||
final Color color;
|
||||
final IconData icon;
|
||||
|
||||
Category(this.name, this.amount, this.color, this.icon);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Transaction {
|
||||
final String category;
|
||||
final double amount;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final DateTime date;
|
||||
final String merchant;
|
||||
|
||||
Transaction(this.category, this.amount, this.icon, this.color, this.date, this.merchant);
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import '../models/transaction.dart';
|
||||
import '../models/category.dart';
|
||||
import '../widgets/summary_item.dart';
|
||||
import '../widgets/expandable_section.dart';
|
||||
import '../widgets/spending_pie_chart.dart';
|
||||
import '../widgets/transaction_list_item.dart';
|
||||
import '../widgets/filter_chip_widget.dart';
|
||||
import 'profile_screen.dart';
|
||||
|
||||
class ExpensesScreen extends StatefulWidget {
|
||||
final Function toggleTheme;
|
||||
final bool isDarkMode;
|
||||
|
||||
const ExpensesScreen({
|
||||
Key? key,
|
||||
required this.toggleTheme,
|
||||
required this.isDarkMode,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ExpensesScreen> createState() => _ExpensesScreenState();
|
||||
}
|
||||
|
||||
class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStateMixin {
|
||||
late AnimationController _listAnimationController;
|
||||
late AnimationController _pieChartAnimationController;
|
||||
late Animation<double> _pieChartAnimation;
|
||||
late AnimationController _pieChartExpandController;
|
||||
late Animation<double> _pieChartHeightFactor;
|
||||
|
||||
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
|
||||
final List<Transaction> _transactions = [
|
||||
Transaction('Groceries', 45.99, Icons.shopping_cart, Colors.green, DateTime.now().subtract(const Duration(days: 1)), 'Whole Foods Market'),
|
||||
Transaction('Subscriptions', 39.99, Icons.subscriptions, Colors.orange, DateTime.now().subtract(const Duration(days: 2)), 'Netflix Premium'),
|
||||
Transaction('Restaurant', 78.50, Icons.restaurant, Colors.red, DateTime.now().subtract(const Duration(days: 2)), 'Italian Corner'),
|
||||
Transaction('Shopping', 132.75, Icons.shopping_bag, Colors.blue, DateTime.now().subtract(const Duration(days: 3)), 'Apple Store'),
|
||||
Transaction('Groceries', 23.45, Icons.shopping_cart, Colors.green, DateTime.now().subtract(const Duration(days: 4)), 'Local Market'),
|
||||
Transaction('Restaurant', 56.80, Icons.restaurant, Colors.red, DateTime.now().subtract(const Duration(days: 5)), 'Sushi Express'),
|
||||
];
|
||||
|
||||
// Use a separate list for the AnimatedList to manage insertions/removals
|
||||
final List<Transaction> _animatedListTransactions = [];
|
||||
|
||||
int _selectedPieIndex = -1;
|
||||
int _selectedNavIndex = 0;
|
||||
bool _isPieChartExpanded = true;
|
||||
bool _isFilterVisible = false;
|
||||
String _selectedFilter = 'All';
|
||||
|
||||
// Sample category data (should ideally be derived from transactions)
|
||||
final List<Category> _categories = [
|
||||
Category('Groceries', 69.44, Colors.green, Icons.shopping_cart), // Sum of Groceries
|
||||
Category('Subscriptions', 39.99, Colors.orange, Icons.subscriptions),
|
||||
Category('Restaurant', 135.30, Colors.red, Icons.restaurant), // Sum of Restaurant
|
||||
Category('Shopping', 132.75, Colors.blue, Icons.shopping_bag),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Controller for list item animations
|
||||
_listAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500), // Adjust duration as needed
|
||||
);
|
||||
|
||||
// Controller for pie chart appearance animation
|
||||
_pieChartAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 800), // Slower fade/scale in
|
||||
);
|
||||
|
||||
_pieChartAnimation = CurvedAnimation(
|
||||
parent: _pieChartAnimationController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
// Controller for pie chart expand/collapse animation
|
||||
_pieChartExpandController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
value: 1.0, // Start expanded
|
||||
);
|
||||
|
||||
_pieChartHeightFactor = CurvedAnimation(
|
||||
parent: _pieChartExpandController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
// Start animations
|
||||
_pieChartAnimationController.forward();
|
||||
_loadInitialTransactions(); // Load transactions with animation
|
||||
}
|
||||
|
||||
void _loadInitialTransactions() {
|
||||
// Animate list items appearing one by one
|
||||
Future.delayed(const Duration(milliseconds: 500), () { // Start list animation after pie chart starts
|
||||
for (int i = 0; i < _transactions.length; i++) {
|
||||
Timer(Duration(milliseconds: 150 * i), () {
|
||||
if (mounted && _listKey.currentState != null) {
|
||||
_animatedListTransactions.add(_transactions[i]);
|
||||
_listKey.currentState!.insertItem(_animatedListTransactions.length - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listAnimationController.dispose();
|
||||
_pieChartAnimationController.dispose();
|
||||
_pieChartExpandController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double get totalExpenses => _categories.fold(0, (sum, item) => sum + item.amount);
|
||||
|
||||
void _togglePieChartVisibility() {
|
||||
setState(() {
|
||||
_isPieChartExpanded = !_isPieChartExpanded;
|
||||
if (_isPieChartExpanded) {
|
||||
_pieChartExpandController.forward();
|
||||
} else {
|
||||
_pieChartExpandController.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleFilterVisibility() {
|
||||
setState(() {
|
||||
_isFilterVisible = !_isFilterVisible;
|
||||
});
|
||||
}
|
||||
|
||||
void _selectPieCategory(int index) {
|
||||
setState(() {
|
||||
_selectedPieIndex = index; // No need to toggle off here, handled by PieTouchData
|
||||
});
|
||||
}
|
||||
|
||||
void _applyFilter(String filter) {
|
||||
setState(() {
|
||||
_selectedFilter = filter;
|
||||
// TODO: Implement actual filtering logic for the transaction list
|
||||
// This might involve removing/inserting items in the AnimatedList
|
||||
// based on the selected filter. For now, just updates the chip state.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final theme = Theme.of(context); // Get theme for easier access
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_balance_wallet,
|
||||
color: isDark ? Colors.greenAccent : Colors.green.shade700,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Finances'), // Title uses AppBarTheme's textStyle
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
widget.isDarkMode ? Icons.wb_sunny_outlined : Icons.nightlight_round,
|
||||
color: widget.isDarkMode ? Colors.yellow : Colors.blue.shade700,
|
||||
),
|
||||
onPressed: () => widget.toggleTheme(),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Hero(
|
||||
tag: 'profileAvatar', // Tag must match the one in ProfileScreen
|
||||
child: Material( // Wrap with Material for Hero animation
|
||||
type: MaterialType.transparency,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.person, color: Colors.green),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => const ProfileScreen(),
|
||||
transitionsBuilder: (_, animation, __, child) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 300), // Adjust duration
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// elevation is handled by AppBarTheme
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Summary Card
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Card( // Uses CardTheme
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Total Expenses',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(fontSize: 16), // Use theme text style
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'\$',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
totalExpenses.toStringAsFixed(2),
|
||||
style: TextStyle(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
SummaryItem( // Use SummaryItem widget
|
||||
icon: Icons.arrow_downward,
|
||||
title: 'Income',
|
||||
amount: '\$2,450.00', // Example data
|
||||
color: Colors.green,
|
||||
),
|
||||
Container(
|
||||
height: 30,
|
||||
width: 1,
|
||||
color: isDark ? Colors.grey.shade700 : Colors.grey.shade300,
|
||||
),
|
||||
SummaryItem( // Use SummaryItem widget
|
||||
icon: Icons.arrow_upward,
|
||||
title: 'Expenses',
|
||||
amount: '\$${totalExpenses.toStringAsFixed(2)}', // Use calculated total
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Pie chart section
|
||||
ExpandableSection( // Use ExpandableSection widget
|
||||
title: 'Spending Breakdown',
|
||||
icon: Icons.pie_chart,
|
||||
isExpanded: _isPieChartExpanded,
|
||||
onTap: _togglePieChartVisibility,
|
||||
heightFactor: _pieChartHeightFactor,
|
||||
child: SpendingPieChart( // Use SpendingPieChart widget
|
||||
categories: _categories,
|
||||
totalExpenses: totalExpenses,
|
||||
selectedPieIndex: _selectedPieIndex,
|
||||
onSelectPieCategory: _selectPieCategory,
|
||||
animation: _pieChartAnimation,
|
||||
),
|
||||
),
|
||||
|
||||
// Transaction list section
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header section
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Recent Transactions',
|
||||
style: theme.textTheme.titleMedium?.copyWith( // Use theme text style
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _toggleFilterVisibility,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.grey.shade800 : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_selectedFilter,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isDark ? Colors.white70 : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
size: 16,
|
||||
color: isDark ? Colors.white70 : Colors.grey.shade700,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Navigate to 'See All' transactions screen
|
||||
},
|
||||
child: Text(
|
||||
'See All',
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.green.shade300 : Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Filter options
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: _isFilterVisible ? 44 : 0,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: _isFilterVisible ? 6 : 0,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
FilterChipWidget(label: 'All', isSelected: _selectedFilter == 'All', onTap: () => _applyFilter('All')),
|
||||
FilterChipWidget(label: 'Groceries', isSelected: _selectedFilter == 'Groceries', onTap: () => _applyFilter('Groceries')),
|
||||
FilterChipWidget(label: 'Subscriptions', isSelected: _selectedFilter == 'Subscriptions', onTap: () => _applyFilter('Subscriptions')),
|
||||
FilterChipWidget(label: 'Restaurant', isSelected: _selectedFilter == 'Restaurant', onTap: () => _applyFilter('Restaurant')),
|
||||
FilterChipWidget(label: 'Shopping', isSelected: _selectedFilter == 'Shopping', onTap: () => _applyFilter('Shopping')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Transactions list
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Card( // Uses CardTheme
|
||||
margin: EdgeInsets.zero,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 200), // Adjust min height as needed
|
||||
child: AnimatedList(
|
||||
key: _listKey,
|
||||
initialItemCount: _animatedListTransactions.length, // Start with items added in initState
|
||||
physics: const NeverScrollableScrollPhysics(), // Disable scrolling within the list itself
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
itemBuilder: (context, index, animation) {
|
||||
// Use the TransactionListItem widget
|
||||
return TransactionListItem(
|
||||
transaction: _animatedListTransactions[index],
|
||||
animation: animation, // Pass animation controller
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16), // Bottom padding inside scroll view
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12), // Extra bottom padding
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
child: BottomNavigationBar( // Uses BottomNavigationBarTheme
|
||||
currentIndex: _selectedNavIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_selectedNavIndex = index;
|
||||
// TODO: Handle navigation based on index
|
||||
});
|
||||
},
|
||||
items: [
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home_rounded),
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Badge( // Example badge
|
||||
label: const Text('3'),
|
||||
child: const Icon(Icons.bar_chart_rounded),
|
||||
),
|
||||
label: 'Reports',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_rounded),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton( // Uses FloatingActionButtonTheme
|
||||
onPressed: () {
|
||||
// TODO: Add new transaction logic
|
||||
},
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder( // Consistent shape
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Profile'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'profileAvatar', // Tag must match the one in ExpensesScreen
|
||||
child: Material( // Wrap with Material for Hero animation
|
||||
type: MaterialType.transparency,
|
||||
child: CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100,
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
size: 50,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'John Doe',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'john.doe@example.com',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isDark ? Colors.green.shade700 : Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text('Edit Profile'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
brightness: Brightness.light,
|
||||
primaryColor: Colors.green,
|
||||
scaffoldBackgroundColor: Colors.grey.shade50,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
brightness: Brightness.light,
|
||||
seedColor: Colors.green,
|
||||
primary: Colors.green,
|
||||
secondary: Colors.green.shade300,
|
||||
surface: Colors.white,
|
||||
background: Colors.grey.shade50,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.green.shade700),
|
||||
titleTextStyle: const TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Montserrat', // Ensure font family is applied here too
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: Colors.green,
|
||||
unselectedItemColor: Colors.grey.shade600,
|
||||
),
|
||||
fontFamily: 'Montserrat',
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: Colors.green.shade400,
|
||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
brightness: Brightness.dark,
|
||||
seedColor: Colors.green,
|
||||
primary: Colors.green.shade400,
|
||||
secondary: Colors.green.shade200,
|
||||
surface: const Color(0xFF222222), // Used for Card background in dark theme
|
||||
background: const Color(0xFF121212),
|
||||
onSurface: Colors.white, // Default text color on surface
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
color: const Color(0xFF1E1E1E), // Darker card color
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xFF1A1A1A), // Slightly different dark background
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.greenAccent),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Montserrat', // Ensure font family is applied here too
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: Colors.green.shade400,
|
||||
foregroundColor: Colors.black87, // Better contrast on light green
|
||||
),
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: const Color(0xFF1A1A1A),
|
||||
selectedItemColor: Colors.green.shade300,
|
||||
unselectedItemColor: Colors.grey.shade500,
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
bodyLarge: TextStyle(color: Colors.white), // Default text style
|
||||
bodyMedium: TextStyle(color: Colors.white70), // Secondary text style
|
||||
// Define other text styles if needed
|
||||
).apply( // Ensure font family is applied globally for text
|
||||
fontFamily: 'Montserrat',
|
||||
),
|
||||
fontFamily: 'Montserrat',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ExpandableSection extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final bool isExpanded;
|
||||
final VoidCallback onTap;
|
||||
final Widget child;
|
||||
final Animation<double> heightFactor;
|
||||
|
||||
const ExpandableSection({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.isExpanded,
|
||||
required this.onTap,
|
||||
required this.heightFactor,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.grey.shade800 : Colors.green.shade100,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: const Radius.circular(16),
|
||||
bottom: Radius.circular(isExpanded ? 0 : 16),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedRotation(
|
||||
turns: isExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: heightFactor,
|
||||
builder: (context, innerChild) {
|
||||
return ClipRect(
|
||||
child: Align(
|
||||
heightFactor: heightFactor.value,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
bottom: Radius.circular(16),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withOpacity(0.3)
|
||||
: Colors.green.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: innerChild,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FilterChipWidget extends StatelessWidget {
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const FilterChipWidget({
|
||||
Key? key,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), // Smaller padding
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (isDark ? Colors.green.shade700 : Colors.green.shade100)
|
||||
: (isDark ? Colors.grey.shade800 : Colors.grey.shade200),
|
||||
borderRadius: BorderRadius.circular(14), // Smaller radius
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: isDark ? Colors.green.shade300 : Colors.green,
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12, // Smaller text
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected
|
||||
? (isDark ? Colors.white : Colors.green.shade700)
|
||||
: (isDark ? Colors.white70 : Colors.grey.shade700),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../models/category.dart';
|
||||
|
||||
class SpendingPieChart extends StatelessWidget {
|
||||
final List<Category> categories;
|
||||
final double totalExpenses;
|
||||
final int selectedPieIndex;
|
||||
final Function(int) onSelectPieCategory;
|
||||
final Animation<double> animation;
|
||||
|
||||
const SpendingPieChart({
|
||||
Key? key,
|
||||
required this.categories,
|
||||
required this.totalExpenses,
|
||||
required this.selectedPieIndex,
|
||||
required this.onSelectPieCategory,
|
||||
required this.animation,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: animation.value,
|
||||
child: Opacity(
|
||||
opacity: animation.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
sections: _generatePieSections(context),
|
||||
pieTouchData: PieTouchData(
|
||||
touchCallback: (FlTouchEvent event, pieTouchResponse) {
|
||||
if (!event.isInterestedForInteractions ||
|
||||
pieTouchResponse == null ||
|
||||
pieTouchResponse.touchedSection == null) {
|
||||
// If touch ends or no section is touched, reset selection
|
||||
if (event is FlPointerUpEvent || event is FlPanEndEvent) {
|
||||
onSelectPieCategory(-1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
onSelectPieCategory(pieTouchResponse.touchedSection!.touchedSectionIndex);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// Центральный интерактивный элемент
|
||||
Positioned.fill(
|
||||
child: selectedPieIndex != -1
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${(categories[selectedPieIndex].amount / totalExpenses * 100).toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'\$${categories[selectedPieIndex].amount.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isDark ? Colors.grey.shade300 : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Обернем легенду в SingleChildScrollView
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(categories.length, (i) {
|
||||
return _buildPieLegendItem(context, i);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Monthly insight text
|
||||
if (selectedPieIndex != -1)
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
'You spent ${categories[selectedPieIndex].amount.toStringAsFixed(2)} on ${categories[selectedPieIndex].name} this month',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: isDark ? Colors.grey.shade300 : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 16), // Placeholder to maintain layout consistency
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPieLegendItem(BuildContext context, int index) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final isSelected = index == selectedPieIndex;
|
||||
final category = categories[index];
|
||||
final fraction = totalExpenses > 0 ? (category.amount / totalExpenses * 100).toStringAsFixed(1) : '0.0';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onSelectPieCategory(isSelected ? -1 : index), // Toggle selection
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (isDark ? Colors.grey.shade800.withOpacity(0.5) : Colors.grey.shade200.withOpacity(0.7))
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: category.color.withOpacity(isDark ? 0.8 : 1.0),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
category.name,
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.white : Colors.grey.shade800,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$fraction%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<PieChartSectionData> _generatePieSections(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return List.generate(categories.length, (i) {
|
||||
final isTouched = i == selectedPieIndex;
|
||||
final double radius = isTouched ? 70 : 60;
|
||||
final category = categories[i];
|
||||
|
||||
return PieChartSectionData(
|
||||
color: isDark ? category.color.withOpacity(0.8) : category.color,
|
||||
value: category.amount,
|
||||
title: '', // Removed title from pie segments for cleaner look
|
||||
radius: radius,
|
||||
badgeWidget: isTouched
|
||||
? Icon(
|
||||
category.icon,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
)
|
||||
: null,
|
||||
badgePositionPercentageOffset: 0.98,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SummaryItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String amount;
|
||||
final Color color;
|
||||
|
||||
const SummaryItem({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.amount,
|
||||
required this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: isDark ? color.withOpacity(0.8) : color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
amount,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/transaction.dart';
|
||||
|
||||
class TransactionListItem extends StatelessWidget {
|
||||
final Transaction transaction;
|
||||
final Animation<double> animation;
|
||||
|
||||
const TransactionListItem({
|
||||
Key? key,
|
||||
required this.transaction,
|
||||
required this.animation,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// Format the date
|
||||
final dateFormatter = DateFormat.MMMd();
|
||||
String formattedDate = dateFormatter.format(transaction.date);
|
||||
|
||||
// Format the time
|
||||
final timeFormatter = DateFormat.jm(); // Formats time like "3:30 PM"
|
||||
String formattedTime = timeFormatter.format(transaction.date);
|
||||
|
||||
// Check if transaction is from today or yesterday
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final yesterday = DateTime(now.year, now.month, now.day - 1);
|
||||
final transactionDate = DateTime(transaction.date.year, transaction.date.month, transaction.date.day);
|
||||
|
||||
if (transactionDate == today) {
|
||||
formattedDate = 'Today';
|
||||
} else if (transactionDate == yesterday) {
|
||||
formattedDate = 'Yesterday';
|
||||
}
|
||||
|
||||
// Ultra-compact transaction item with slide transition
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(-1, 0),
|
||||
end: const Offset(0, 0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOut,
|
||||
)),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 6, top: 2), // Even smaller margins
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 65, // Fixed height to ensure consistency
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
onTap: () {
|
||||
// Handle tap, e.g., navigate to transaction details
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0), // Smaller padding
|
||||
child: Row(
|
||||
children: [
|
||||
// Leading icon
|
||||
Container(
|
||||
width: 36, // Smaller icon container
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: transaction.color.withOpacity(isDark ? 0.2 : 0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
transaction.icon,
|
||||
color: transaction.color.withOpacity(isDark ? 0.9 : 1.0),
|
||||
size: 16, // Smaller icon
|
||||
),
|
||||
),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Title and merchant on same line
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
transaction.category,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'·', // Bullet separator
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
transaction.merchant,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Date
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 10,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Amount and Time
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'- \$${transaction.amount.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.redAccent.shade100 : Colors.red.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formattedTime,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user