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) {
+1 -9
View File
@@ -49,14 +49,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.dev"
source: hosted
version: "1.0.6"
fake_async:
dependency: transitive
description:
@@ -233,4 +225,4 @@ packages:
source: hosted
version: "0.3.0"
sdks:
dart: ">=3.2.6 <4.0.0"
dart: ">=3.2.0-194.0.dev <4.0.0"
+5 -62
View File
@@ -1,81 +1,27 @@
name: expense_tracker
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
description: A new Flutter project.
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=3.2.6 <4.0.0'
sdk: ">=3.0.0 <4.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
uuid: ^4.3.3
intl: ^0.19.0
uuid: ^4.2.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# fonts:
# - family: Schyler
# fonts:
@@ -87,6 +33,3 @@ flutter:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages