Abschnitt 5
This commit is contained in:
+48
-1
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user