Вот рекомендуемое сообщение коммита:

```
feat: обновить интерфейс с использованием библиотеки Forui
```

Это сообщение коммита:
- Начинается с `feat:`, что указывает на новую функциональность
- Кратко описывает основное изменение - обновление интерфейса с использованием библиотеки Forui
- Написано на русском языке
- Соответствует стандартам conventional commits
- Уложено в рекомендованные 72 символа
This commit is contained in:
2025-06-08 00:27:42 +03:00
parent 90e5ac0c46
commit 6eef7e95be
8 changed files with 131 additions and 265 deletions
+33 -27
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import '../database/database.dart' as db;
import '../models/category.dart';
@@ -102,22 +103,19 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
try {
await _controller.addSampleTransaction();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Transaction added successfully'),
duration: Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
FToast.show(
context: context,
title: const Text('Success'),
description: const Text('Transaction added successfully'),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error adding transaction: $e'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
FToast.show(
context: context,
title: const Text('Error'),
description: Text('Error adding transaction: $e'),
style: FToastStyle.destructive,
);
}
}
@@ -125,10 +123,8 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: _buildAppBar(isDark),
appBar: _buildAppBar(context),
body: StreamBuilder<List<Category>>(
stream: _controller.watchCategoryTotals(),
builder: (context, categorySnapshot) {
@@ -183,24 +179,30 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
},
),
bottomNavigationBar: _buildBottomNavigationBar(),
floatingActionButton: FloatingActionButton.extended(
onPressed: _addSampleTransaction,
tooltip: 'Add Transaction',
icon: const Icon(Icons.add),
label: const Text('Add'),
floatingActionButton: FButton(
label: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add),
SizedBox(width: 8),
Text('Add'),
],
),
style: FButtonStyle.primary,
onPress: _addSampleTransaction,
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
);
}
AppBar _buildAppBar(bool isDark) {
AppBar _buildAppBar(BuildContext context) {
return AppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.account_balance_wallet_outlined,
color: isDark ? Colors.greenAccent.shade100 : Colors.green.shade800,
color: context.theme.colorScheme.primary,
size: 24,
),
const SizedBox(width: 8),
@@ -209,7 +211,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
),
centerTitle: true,
leading: IconButton(
tooltip: isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode',
tooltip: widget.isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode',
icon: Icon(
widget.isDarkMode ? Icons.wb_sunny_outlined : Icons.nightlight_round,
color: widget.isDarkMode ? Colors.yellow.shade300 : Colors.blue.shade700,
@@ -225,10 +227,14 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
type: MaterialType.transparency,
child: IconButton(
tooltip: 'View Profile',
icon: CircleAvatar(
radius: 18,
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100,
child: const Icon(Icons.person_outline, color: Colors.green, size: 20),
icon: FAvatar(
size: 36,
backgroundColor: context.theme.colorScheme.primary.withOpacity(0.1),
child: Icon(
Icons.person_outline,
color: context.theme.colorScheme.primary,
size: 20,
),
),
onPressed: () {
Navigator.push(
+14 -24
View File
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.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'),
@@ -24,13 +23,13 @@ class ProfileScreen extends StatelessWidget {
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(
child: FAvatar(
size: 100,
backgroundColor: context.theme.colorScheme.primary.withOpacity(0.1),
child: Icon(
Icons.person,
size: 50,
color: Colors.green,
color: context.theme.colorScheme.primary,
),
),
),
@@ -38,32 +37,23 @@ class ProfileScreen extends StatelessWidget {
const SizedBox(height: 24),
Text(
'John Doe',
style: TextStyle(
fontSize: 24,
style: context.theme.typography.xl2.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black,
color: context.theme.colorScheme.foreground,
),
),
const SizedBox(height: 8),
Text(
'john.doe@example.com',
style: TextStyle(
fontSize: 16,
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
style: context.theme.typography.base.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
),
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'),
FButton(
label: const Text('Edit Profile'),
style: FButtonStyle.primary,
onPress: () {},
),
],
),
+21 -73
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class ExpandableSection extends StatelessWidget {
final String title;
@@ -20,82 +21,29 @@ class ExpandableSection extends StatelessWidget {
@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),
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: FCollapsible(
initiallyExpanded: isExpanded,
title: Row(
children: [
Icon(
icon,
color: context.theme.colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
Text(
title,
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.bold,
color: context.theme.colorScheme.primary,
),
),
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,
),
],
child: child,
),
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class FilterChipWidget extends StatelessWidget {
final String label;
@@ -14,35 +15,14 @@ class FilterChipWidget extends StatelessWidget {
@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),
),
),
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FButton(
label: Text(label),
style: isSelected
? FButtonStyle.primary
: FButtonStyle.secondary,
onPress: onTap,
),
);
}
+13 -25
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:intl/intl.dart';
import 'summary_item.dart';
@@ -12,32 +13,23 @@ class SummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Card(
elevation: 2,
child: FCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'Total Expenses This Period',
style: theme.textTheme.titleMedium,
style: context.theme.typography.lg,
),
const SizedBox(height: 8),
_buildTotalAmount(theme),
_buildTotalAmount(context),
const SizedBox(height: 16),
Divider(
height: 1,
thickness: 1,
indent: 20,
endIndent: 20,
color: theme.dividerColor.withOpacity(0.5),
),
FSeparator(),
const SizedBox(height: 16),
_buildSummaryItems(theme),
_buildSummaryItems(),
],
),
),
@@ -45,33 +37,29 @@ class SummaryCard extends StatelessWidget {
);
}
Widget _buildTotalAmount(ThemeData theme) {
Widget _buildTotalAmount(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'\$',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: theme.colorScheme.primary,
style: context.theme.typography.xl.copyWith(
color: context.theme.colorScheme.primary,
),
),
Text(
NumberFormat.currency(symbol: '', decimalDigits: 2).format(totalExpenses),
style: TextStyle(
fontSize: 36,
style: context.theme.typography.xl4.copyWith(
color: context.theme.colorScheme.primary,
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
letterSpacing: -1,
),
),
],
);
}
Widget _buildSummaryItems(ThemeData theme) {
Widget _buildSummaryItems() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -84,7 +72,7 @@ class SummaryCard extends StatelessWidget {
Container(
height: 35,
width: 1,
color: theme.dividerColor.withOpacity(0.5),
color: Colors.grey.withOpacity(0.3),
),
SummaryItem(
icon: Icons.arrow_upward_rounded,
+6 -8
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class SummaryItem extends StatelessWidget {
final IconData icon;
@@ -16,7 +17,6 @@ class SummaryItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column(
children: [
Row(
@@ -24,14 +24,13 @@ class SummaryItem extends StatelessWidget {
Icon(
icon,
size: 16,
color: isDark ? color.withOpacity(0.8) : color,
color: color,
),
const SizedBox(width: 4),
Text(
title,
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
),
],
@@ -39,10 +38,9 @@ class SummaryItem extends StatelessWidget {
const SizedBox(height: 4),
Text(
amount,
style: TextStyle(
fontSize: 16,
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
color: context.theme.colorScheme.foreground,
),
),
],
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:intl/intl.dart';
import '../database/database.dart' as db; // Import database with prefix 'db'
import '../models/transaction_record.dart';
@@ -16,9 +17,6 @@ class TransactionListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context);
// Get category details (icon, color) using the utility
final categoryDetails = CategoryUtils.getCategoryDetails(transaction.categoryName);
@@ -42,11 +40,6 @@ class TransactionListItem extends StatelessWidget {
displayDate = '${dateFormatter.format(transaction.date)}, ${timeFormatter.format(transaction.date)}';
}
// Determine text colors based on theme
Color primaryTextColor = theme.textTheme.bodyLarge?.color ?? (isDark ? Colors.white : Colors.black87);
Color secondaryTextColor = theme.textTheme.bodyMedium?.color ?? (isDark ? Colors.white70 : Colors.grey.shade600);
Color amountColor = isDark ? Colors.red.shade200 : Colors.red.shade700; // Expense color
// Use FadeTransition for item appearance (works with ListView.builder)
return FadeTransition(
opacity: animation, // Apply fade animation
@@ -60,16 +53,12 @@ class TransactionListItem extends StatelessWidget {
child: Row(
children: [
// Icon container with category color/icon
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: categoryDetails.colorCode.withOpacity(isDark ? 0.25 : 0.15), // Use category color with opacity
borderRadius: BorderRadius.circular(12), // Rounded corners
),
FAvatar(
backgroundColor: categoryDetails.colorCode.withOpacity(0.15),
child: Icon(
categoryDetails.iconCode, // Use category icon
color: categoryDetails.colorCode, // Use category color for icon
size: 20, // Icon size
categoryDetails.iconCode,
color: categoryDetails.colorCode,
size: 20,
),
),
const SizedBox(width: 12), // Spacing
@@ -82,9 +71,9 @@ class TransactionListItem extends StatelessWidget {
// Display Merchant if available, otherwise Category Name
Text(
transaction.merchant.isNotEmpty ? transaction.merchant : transaction.categoryName,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500, // Medium weight for primary text
color: primaryTextColor,
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.w500,
color: context.theme.colorScheme.foreground,
),
maxLines: 1, // Prevent wrapping
overflow: TextOverflow.ellipsis, // Handle long text
@@ -93,9 +82,8 @@ class TransactionListItem extends StatelessWidget {
// Display formatted date/time
Text(
displayDate,
style: theme.textTheme.bodyMedium?.copyWith(
color: secondaryTextColor, // Lighter color for secondary text
fontSize: 12, // Smaller font size
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -109,9 +97,9 @@ class TransactionListItem extends StatelessWidget {
Text(
// Format amount as currency (negative for expense)
NumberFormat.currency(symbol: '-\$', decimalDigits: 2).format(transaction.amount),
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600, // Bold weight for amount
color: amountColor, // Use expense color
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.w600,
color: context.theme.colorScheme.destructive,
),
),
],
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import '../database/database.dart' as db;
import '../models/transaction_record.dart' as model;
import '../utils/category_utils.dart';
@@ -23,21 +24,18 @@ class TransactionsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(theme, isDark),
_buildHeader(context),
_buildFilterChips(),
_buildTransactionsList(theme),
_buildTransactionsList(context),
const SizedBox(height: 16),
],
);
}
Widget _buildHeader(ThemeData theme, bool isDark) {
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 4),
child: Row(
@@ -45,37 +43,21 @@ class TransactionsSection extends StatelessWidget {
children: [
Text(
'Recent Transactions',
style: theme.textTheme.titleLarge?.copyWith(
style: context.theme.typography.xl.copyWith(
fontWeight: FontWeight.w600,
),
),
InkWell(
onTap: onToggleFilter,
borderRadius: BorderRadius.circular(16),
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: 13,
color: isDark ? Colors.white70 : Colors.black87,
),
),
const SizedBox(width: 4),
Icon(
Icons.filter_list_alt,
size: 18,
color: isDark ? Colors.white70 : Colors.black87,
),
],
),
FButton(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(selectedFilter),
const SizedBox(width: 4),
const Icon(Icons.filter_list_alt, size: 18),
],
),
style: FButtonStyle.secondary,
onPress: onToggleFilter,
),
],
),
@@ -113,20 +95,10 @@ class TransactionsSection extends StatelessWidget {
);
}
Widget _buildTransactionsList(ThemeData theme) {
Widget _buildTransactionsList(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Card(
margin: EdgeInsets.zero,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: theme.dividerColor.withOpacity(0.5),
width: 1,
),
),
clipBehavior: Clip.antiAlias,
child: FCard(
child: StreamBuilder<List<db.Transaction>>(
stream: transactionsStream,
builder: (context, snapshot) {
@@ -154,7 +126,9 @@ class TransactionsSection extends StatelessWidget {
selectedFilter == 'All'
? 'No transactions yet.'
: 'No transactions found for $selectedFilter.',
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey),
style: context.theme.typography.base.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
),
),
);
@@ -165,13 +139,7 @@ class TransactionsSection extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8.0),
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 1,
indent: 16,
endIndent: 16,
color: theme.dividerColor.withOpacity(0.3),
),
separatorBuilder: (context, index) => FSeparator(),
itemBuilder: (context, index) {
final dbTransaction = transactions[index];
final categoryDetails = CategoryUtils.getCategoryDetails(