Abschnitt 5

This commit is contained in:
2024-02-13 13:51:39 +01:00
parent 6e41e1a309
commit 8aa2252d90
10 changed files with 292 additions and 88 deletions
+48 -1
View File
@@ -1,10 +1,57 @@
import 'package:flutter/material.dart';
import 'package:expense_tracker/widgets/expenses.dart';
var kColorScheme = ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 96, 59, 181),
);
var kDarkColorScheme = ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: const Color.fromARGB(255, 5, 99, 125),
);
void main() {
runApp(
MaterialApp(
theme: ThemeData(useMaterial3: true),
darkTheme: ThemeData.dark().copyWith(
useMaterial3: true,
colorScheme: kDarkColorScheme,
cardTheme: const CardTheme().copyWith(
color: kDarkColorScheme.secondaryContainer,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: kDarkColorScheme.primaryContainer,
foregroundColor: kDarkColorScheme.onPrimaryContainer,
),
),
),
theme: ThemeData().copyWith(
useMaterial3: true,
colorScheme: kColorScheme,
appBarTheme: const AppBarTheme().copyWith(
backgroundColor: kColorScheme.onPrimaryContainer,
foregroundColor: kColorScheme.primaryContainer,
),
cardTheme: const CardTheme().copyWith(
color: kColorScheme.secondaryContainer,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: kColorScheme.primaryContainer,
),
),
textTheme: ThemeData().textTheme.copyWith(
titleLarge: TextStyle(
fontWeight: FontWeight.bold,
color: kColorScheme.onSecondaryContainer,
fontSize: 16,
),
),
),
themeMode: ThemeMode.light,
home: const Expenses(),
),
);
+27 -2
View File
@@ -6,10 +6,10 @@ import 'package:intl/intl.dart';
final formatter = DateFormat('dd.MM.yyyy');
const uuid = Uuid();
enum Category { foot, travel, leisure, work }
enum Category { food, travel, leisure, work }
const categoryIcons = {
Category.foot: Icons.lunch_dining,
Category.food: Icons.lunch_dining,
Category.travel: Icons.flight_takeoff,
Category.leisure: Icons.movie,
Category.work: Icons.work,
@@ -33,3 +33,28 @@ class Expense {
return formatter.format(date);
}
}
class ExpenseBucket {
const ExpenseBucket({
required this.category,
required this.expenses,
});
ExpenseBucket.forCategory(List<Expense> allExpenses, this.category)
: expenses = allExpenses
.where((expense) => expense.category == category)
.toList();
final Category category;
final List<Expense> expenses;
double get totalExpenses {
double sum = 0;
for (final expense in expenses) {
sum += expense.amount;
}
return sum;
}
}
+96
View File
@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:expense_tracker/widgets/chart/chart_bar.dart';
import 'package:expense_tracker/models/expense.dart';
class Chart extends StatelessWidget {
const Chart({super.key, required this.expenses});
final List<Expense> expenses;
List<ExpenseBucket> get buckets {
return [
ExpenseBucket.forCategory(expenses, Category.food),
ExpenseBucket.forCategory(expenses, Category.leisure),
ExpenseBucket.forCategory(expenses, Category.travel),
ExpenseBucket.forCategory(expenses, Category.work),
];
}
double get maxTotalExpense {
double maxTotalExpense = 0;
for (final bucket in buckets) {
if (bucket.totalExpenses > maxTotalExpense) {
maxTotalExpense = bucket.totalExpenses;
}
}
return maxTotalExpense;
}
@override
Widget build(BuildContext context) {
final isDarkMode =
MediaQuery.of(context).platformBrightness == Brightness.dark;
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 8,
),
width: double.infinity,
height: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.3),
Theme.of(context).colorScheme.primary.withOpacity(0.0)
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
child: Column(
children: [
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (final bucket in buckets) // alternative to map()
ChartBar(
fill: bucket.totalExpenses == 0
? 0
: bucket.totalExpenses / maxTotalExpense,
)
],
),
),
const SizedBox(height: 12),
Row(
children: buckets
.map(
(bucket) => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(
categoryIcons[bucket.category],
color: isDarkMode
? Theme.of(context).colorScheme.secondary
: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.7),
),
),
),
)
.toList(),
)
],
),
);
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class ChartBar extends StatelessWidget {
const ChartBar({
super.key,
required this.fill,
});
final double fill;
@override
Widget build(BuildContext context) {
final isDarkMode =
MediaQuery.of(context).platformBrightness == Brightness.dark;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: FractionallySizedBox(
heightFactor: fill,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(8)),
color: isDarkMode
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary.withOpacity(0.65),
),
),
),
),
);
}
}
+43 -3
View File
@@ -2,6 +2,7 @@ import 'package:expense_tracker/widgets/expenses_list/expenses_list.dart';
import 'package:expense_tracker/widgets/new_expense.dart';
import 'package:flutter/material.dart';
import 'package:expense_tracker/models/expense.dart';
import 'package:expense_tracker/widgets/chart/chart.dart';
class Expenses extends StatefulWidget {
const Expenses({super.key});
@@ -30,11 +31,50 @@ class _ExpensesState extends State<Expenses> {
void _openAddExpenseOverlay() {
showModalBottomSheet(
context: context, builder: (ctx) => const NewExpense());
isScrollControlled: true,
context: context,
builder: (ctx) => NewExpense(onAddExpense: _addExpense),
);
}
void _addExpense(Expense expense) {
setState(() {
_registeredExpenses.add(expense);
});
}
void _removeExpense(Expense expense) {
final expenseIndex = _registeredExpenses.indexOf(expense);
setState(() {
_registeredExpenses.remove(expense);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 3),
content: const Text('Expense deleted.'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
setState(() {
_registeredExpenses.insert(expenseIndex, expense);
});
},
),
),
);
}
@override
Widget build(BuildContext context) {
Widget mainContent = const Center(
child: Text('No expenses found. Start adding some!'),
);
if (_registeredExpenses.isNotEmpty) {
mainContent = ExpensesList(
expenses: _registeredExpenses, onRemoveExpense: _removeExpense);
}
return Scaffold(
appBar: AppBar(
title: const Text('Flutter ExpenseTracker'),
@@ -47,9 +87,9 @@ class _ExpensesState extends State<Expenses> {
),
body: Column(
children: [
const Text('The chart'),
Chart(expenses: _registeredExpenses),
Expanded(
child: ExpensesList(expenses: _registeredExpenses),
child: mainContent,
),
],
),
+5 -1
View File
@@ -15,8 +15,12 @@ class ExpenseItem extends StatelessWidget {
vertical: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(expense.title),
Text(
expense.title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Row(
children: [
+14 -1
View File
@@ -6,15 +6,28 @@ class ExpensesList extends StatelessWidget {
const ExpensesList({
super.key,
required this.expenses,
required this.onRemoveExpense,
});
final List<Expense> expenses;
final void Function(Expense expense) onRemoveExpense;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: expenses.length,
itemBuilder: (ctx, index) => ExpenseItem(expenses[index]),
itemBuilder: (ctx, index) => Dismissible(
key: ValueKey(expenses[index]),
background: Container(
color: Theme.of(context).colorScheme.error.withOpacity(0.75),
margin: EdgeInsets.symmetric(
horizontal: Theme.of(context).cardTheme.margin!.horizontal),
),
onDismissed: (direction) {
onRemoveExpense(expenses[index]);
},
child: ExpenseItem(expenses[index]),
),
);
}
}
+18 -9
View File
@@ -1,4 +1,5 @@
import 'package:expense_tracker/models/expense.dart';
import 'package:expense_tracker/widgets/expenses.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
@@ -6,7 +7,9 @@ import 'package:intl/intl.dart';
final formatter = DateFormat('dd.MM.yyyy');
class NewExpense extends StatefulWidget {
const NewExpense({super.key});
const NewExpense({super.key, required this.onAddExpense});
final void Function(Expense expense) onAddExpense;
@override
State<StatefulWidget> createState() {
@@ -60,7 +63,13 @@ class _NewExpenseState extends State<NewExpense> {
return;
}
// ...
widget.onAddExpense(Expense(
title: _titleController.text,
amount: enteredAmount,
date: _selectedDate!,
category: _selectedCategory,
));
Navigator.pop(context);
}
@override
@@ -72,7 +81,7 @@ class _NewExpenseState extends State<NewExpense> {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
child: Column(
children: [
TextField(
@@ -124,12 +133,12 @@ class _NewExpenseState extends State<NewExpense> {
items: Category.values
.map(
(category) => DropdownMenuItem(
value: category,
child: Text(
category.name.toUpperCase(),
),
),
)
value: category,
child: Text(
category.name.toUpperCase(),
),
),
)
.toList(),
onChanged: (value) {
if (value == null) {