From 8aa2252d907bf56f56166fde5bc3adb257ec599a Mon Sep 17 00:00:00 2001 From: Herwig Birke Date: Tue, 13 Feb 2024 13:51:39 +0100 Subject: [PATCH] Abschnitt 5 --- lib/main.dart | 49 +++++++++- lib/models/expense.dart | 29 +++++- lib/widgets/chart/chart.dart | 96 ++++++++++++++++++++ lib/widgets/chart/chart_bar.dart | 35 +++++++ lib/widgets/expenses.dart | 46 +++++++++- lib/widgets/expenses_list/expense_item.dart | 6 +- lib/widgets/expenses_list/expenses_list.dart | 15 ++- lib/widgets/new_expense.dart | 27 ++++-- pubspec.lock | 10 +- pubspec.yaml | 67 +------------- 10 files changed, 292 insertions(+), 88 deletions(-) create mode 100644 lib/widgets/chart/chart.dart create mode 100644 lib/widgets/chart/chart_bar.dart diff --git a/lib/main.dart b/lib/main.dart index 00cc6ed..e392620 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), ), ); diff --git a/lib/models/expense.dart b/lib/models/expense.dart index b7741fa..57828a7 100644 --- a/lib/models/expense.dart +++ b/lib/models/expense.dart @@ -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 allExpenses, this.category) + : expenses = allExpenses + .where((expense) => expense.category == category) + .toList(); + + final Category category; + final List expenses; + + double get totalExpenses { + double sum = 0; + + for (final expense in expenses) { + sum += expense.amount; + } + + return sum; + } +} diff --git a/lib/widgets/chart/chart.dart b/lib/widgets/chart/chart.dart new file mode 100644 index 0000000..31c4cf7 --- /dev/null +++ b/lib/widgets/chart/chart.dart @@ -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 expenses; + + List 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(), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/chart/chart_bar.dart b/lib/widgets/chart/chart_bar.dart new file mode 100644 index 0000000..793df63 --- /dev/null +++ b/lib/widgets/chart/chart_bar.dart @@ -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), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/expenses.dart b/lib/widgets/expenses.dart index 22ab843..ce53586 100644 --- a/lib/widgets/expenses.dart +++ b/lib/widgets/expenses.dart @@ -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 { 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 { ), body: Column( children: [ - const Text('The chart'), + Chart(expenses: _registeredExpenses), Expanded( - child: ExpensesList(expenses: _registeredExpenses), + child: mainContent, ), ], ), diff --git a/lib/widgets/expenses_list/expense_item.dart b/lib/widgets/expenses_list/expense_item.dart index 6b53c31..9f8292c 100644 --- a/lib/widgets/expenses_list/expense_item.dart +++ b/lib/widgets/expenses_list/expense_item.dart @@ -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: [ diff --git a/lib/widgets/expenses_list/expenses_list.dart b/lib/widgets/expenses_list/expenses_list.dart index eb346a0..475fe38 100644 --- a/lib/widgets/expenses_list/expenses_list.dart +++ b/lib/widgets/expenses_list/expenses_list.dart @@ -6,15 +6,28 @@ class ExpensesList extends StatelessWidget { const ExpensesList({ super.key, required this.expenses, + required this.onRemoveExpense, }); final List 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]), + ), ); } } diff --git a/lib/widgets/new_expense.dart b/lib/widgets/new_expense.dart index 7e568da..b619003 100644 --- a/lib/widgets/new_expense.dart +++ b/lib/widgets/new_expense.dart @@ -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 createState() { @@ -60,7 +63,13 @@ class _NewExpenseState extends State { 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 { 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 { 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) { diff --git a/pubspec.lock b/pubspec.lock index b7cfda4..56e1811 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index de9bc72..ab921f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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