diff --git a/LICENSE b/LICENSE index 6f65543..5d8884a 100644 --- a/LICENSE +++ b/LICENSE @@ -653,11 +653,11 @@ Also add information on how to contact you by electronic and paper mail. notice like this when it starts in an interactive mode: Spritverbrauch Copyright (C) 2024 RedCommander735 - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w`. This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. + under certain conditions; type `show c` for details. -The hypothetical commands `show w' and `show c' should show the appropriate +The hypothetical commands `show w` and `show c` should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". diff --git a/README.md b/README.md index 3632e3b..b6cc745 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ A simple app to keep track of how much fuel you're using on average. - [x] Database and backend stuff (actual function) - [x] Prices with 3rd digit ^ - [x] Time date selector for overview -- [ ] Settings page via breadcrumbs - - [ ] App info - - [ ] Backup to csv/xslx/...? +- [x] Settings page via breadcrumbs #### Extra stuff: - [ ] Edit Items on longpress instead of deleting @@ -26,4 +24,6 @@ A simple app to keep track of how much fuel you're using on average. - [ ] Time date selector for overview - [ ] Add option set starting date from listview item - [ ] Settings page via breadcrumbs + - [ ] File explorer for csv import + - [ ] Option to only load elements and do not delete old - [ ] Color Theme changer \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 25d77b6..cf64414 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "de.redcommander735.spritverbrauch" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8a5b95c..5189fb9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + ItemListModel()), - ChangeNotifierProvider(create: (BuildContext context) => FilterModel()) + ChangeNotifierProvider(create: (BuildContext context) => FilterModel()), + ChangeNotifierProvider(create: (BuildContext context) => SettingsModel()), ], child: const Spritpreise()), ); } @@ -26,11 +31,11 @@ void main() async { class Spritpreise extends StatefulWidget { const Spritpreise({super.key}); - static final _defaultLightColorScheme = ColorScheme.fromSeed( - seedColor: Colors.blue[900]!, brightness: Brightness.light); + static final _defaultLightColorScheme = + ColorScheme.fromSeed(seedColor: Colors.blue[900]!, brightness: Brightness.light); - static final _defaultDarkColorScheme = ColorScheme.fromSeed( - seedColor: Colors.blue[900]!, brightness: Brightness.dark); + static final _defaultDarkColorScheme = + ColorScheme.fromSeed(seedColor: Colors.blue[900]!, brightness: Brightness.dark, background: Colors.black); @override State createState() => _SpritpreiseState(); @@ -61,10 +66,13 @@ class _SpritpreiseState extends State { debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: lightColorScheme ?? Spritpreise._defaultLightColorScheme, + scaffoldBackgroundColor: Spritpreise._defaultLightColorScheme.background, useMaterial3: true, ), darkTheme: ThemeData( colorScheme: darkColorScheme ?? Spritpreise._defaultDarkColorScheme, + appBarTheme: AppBarTheme(backgroundColor: Spritpreise._defaultDarkColorScheme.background), + scaffoldBackgroundColor: Spritpreise._defaultDarkColorScheme.background, useMaterial3: true, ), themeMode: ThemeMode.system, @@ -118,38 +126,51 @@ class Main extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Consumer2( - builder: (BuildContext context, FilterModel filterModel, - ItemListModel itemListModel, Widget? child) { - return Row( + builder: (BuildContext context, FilterModel filterModel, ItemListModel itemListModel, + Widget? child) { + return Column( children: [ - IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const Filter(), - ), - ); - }, - icon: const Icon(Icons.tune), - ), - if (filterModel.filterEnabled) - const Text( - 'Filter aktiv', - style: TextStyle(fontSize: 14), - ), - if (filterModel.filterEnabled && itemListModel.hiddenEntries > 0) - Text( - ', ausgeblendete Enträge: ${itemListModel.hiddenEntries}', - style: const TextStyle(fontSize: 14), + Row(children: [ + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Settings(), + ), + ); + }, + icon: const Icon(Icons.settings), ), - if (filterModel.filterEnabled) IconButton( onPressed: () { - filterModel.setFilterEnabled(false); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Filter(), + ), + ); }, - icon: const Icon(Icons.close), - ) + icon: const Icon(Icons.tune), + ), + if (filterModel.filterEnabled) + const Text( + 'Filter aktiv', + style: TextStyle(fontSize: 14), + ), + if (filterModel.filterEnabled && itemListModel.hiddenEntries > 0) + Text( + ', ausgeblendet: ${itemListModel.hiddenEntries}', + style: const TextStyle(fontSize: 14), + ), + if (filterModel.filterEnabled) + IconButton( + onPressed: () { + filterModel.setFilterEnabled(false); + }, + icon: const Icon(Icons.close), + ), + ]), ], ); }, @@ -164,7 +185,7 @@ class Main extends StatelessWidget { ), Center( child: Padding( - padding: EdgeInsets.symmetric(vertical: 80), + padding: EdgeInsets.only(top: 80), child: FractionallySizedBox( widthFactor: 0.55, child: Overview(), diff --git a/lib/src/add_item.dart b/lib/src/add_item.dart index 34b10ff..6945e6b 100644 --- a/lib/src/add_item.dart +++ b/lib/src/add_item.dart @@ -36,7 +36,10 @@ class _AddItemState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Eintrag hinzufügen')), + appBar: AppBar( + title: const Text('Eintrag hinzufügen'), + centerTitle: true, + ), body: Form( key: _formKey, child: Scrollbar( @@ -52,11 +55,9 @@ class _AddItemState extends State { filled: true, labelText: 'Datum', prefixIcon: const Icon(Icons.calendar_today), - enabledBorder: - const OutlineInputBorder(borderSide: BorderSide.none), + enabledBorder: const OutlineInputBorder(borderSide: BorderSide.none), focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary), + borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), ), ), readOnly: true, @@ -153,8 +154,7 @@ class _AddItemState extends State { litersPerKilometer: litersPerKilometer, ); - Provider.of(context, listen: false) - .add(item); + Provider.of(context, listen: false).add(item); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text("Eintrag hinzugefügt"), diff --git a/lib/src/components/font_awesome.dart b/lib/src/components/font_awesome.dart new file mode 100644 index 0000000..217b9db --- /dev/null +++ b/lib/src/components/font_awesome.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +class FontAwesomeBrands { + FontAwesomeBrands._(); + + static const _kFontFam = 'FontAwesomeBrands'; + + static const IconData github = IconData(0xf09b, fontFamily: _kFontFam); +} diff --git a/lib/src/components/settings/settings_group.dart b/lib/src/components/settings/settings_group.dart new file mode 100644 index 0000000..4b8a99f --- /dev/null +++ b/lib/src/components/settings/settings_group.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class SettingsGroup extends StatelessWidget { + const SettingsGroup({super.key, required this.title, required this.children}); + + final String title; + final List children; + + @override + Widget build(BuildContext context) { + return Column( + children: List.from([ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, top: 16, bottom: 4), + child: Text( + title, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + fontSize: DefaultTextStyle.of(context).style.fontSize! * 17 / 20), + ), + ), + ], + ) + ]) + ..addAll(children)..add(const Divider( + height: 0, + )), + ); + } +} diff --git a/lib/src/components/settings/settings_item.dart b/lib/src/components/settings/settings_item.dart new file mode 100644 index 0000000..f776aeb --- /dev/null +++ b/lib/src/components/settings/settings_item.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class SettingsItem extends StatelessWidget { + const SettingsItem({ + super.key, + this.icon, + this.title, + this.subtitle, + this.onTap, + }); + + final IconData? icon; + final String? title; + final String? subtitle; + final void Function()? onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: (onTap == null) ? () {} : onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 72, + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon(icon), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) Text(title!), + if (subtitle != null) + Text( + subtitle!, + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7), + fontSize: DefaultTextStyle.of(context).style.fontSize! * 9 / 10), + softWrap: true, + ), + ], + ) + ]), + ), + ), + ); + } +} diff --git a/lib/src/components/settings/settings_topic.dart b/lib/src/components/settings/settings_topic.dart new file mode 100644 index 0000000..e4be63b --- /dev/null +++ b/lib/src/components/settings/settings_topic.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class SettingsTopic extends StatelessWidget { + const SettingsTopic({ + super.key, + this.icon, + this.title, + this.onTap, + }); + + final IconData? icon; + final String? title; + final void Function()? onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: (onTap == null) ? () {} : onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 64, + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon(icon), + ), + Text((title == null) ? '' : title!) + ]), + ), + ), + ); + } +} diff --git a/lib/src/components/settings/settings_topic_page.dart b/lib/src/components/settings/settings_topic_page.dart new file mode 100644 index 0000000..e947b98 --- /dev/null +++ b/lib/src/components/settings/settings_topic_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class SettingsTopicPage extends StatelessWidget { + const SettingsTopicPage({super.key, required this.title, required this.children, this.textSize = 16}); + + final double textSize; + + final String title; + final List children; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(title), + centerTitle: true, + ), + body: DefaultTextStyle( + style: TextStyle(fontSize: textSize, color: Theme.of(context).colorScheme.onBackground), + child: Column( + children: children, + ), + ), + ); + } +} diff --git a/lib/src/components/sp_button.dart b/lib/src/components/sp_button.dart index f2fe9e1..45bc59e 100644 --- a/lib/src/components/sp_button.dart +++ b/lib/src/components/sp_button.dart @@ -6,24 +6,17 @@ class SPButton extends StatelessWidget { final double width; final bool primary; - const SPButton(this.text, - {super.key, - required this.onPressed, - this.width = 150, - this.primary = false}); + const SPButton(this.text, {super.key, required this.onPressed, this.width = 150, this.primary = false}); @override Widget build(BuildContext context) { return ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: - primary ? Theme.of(context).colorScheme.primary : null, - fixedSize: Size.fromWidth(width)), + backgroundColor: primary ? Theme.of(context).colorScheme.primary : null, fixedSize: Size.fromWidth(width)), onPressed: onPressed, child: Text( text, - style: TextStyle( - color: primary ? Theme.of(context).colorScheme.onPrimary : null), + style: TextStyle(color: primary ? Theme.of(context).colorScheme.onPrimary : null), ), ); } @@ -35,24 +28,17 @@ class SPDynButton extends StatelessWidget { final double padding; final bool primary; - const SPDynButton(this.text, - {super.key, - required this.onPressed, - this.padding = 16, - this.primary = false}); + const SPDynButton(this.text, {super.key, required this.onPressed, this.padding = 16, this.primary = false}); @override Widget build(BuildContext context) { return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: - primary ? Theme.of(context).colorScheme.primary : null), + style: ElevatedButton.styleFrom(backgroundColor: primary ? Theme.of(context).colorScheme.primary : null), child: Padding( padding: EdgeInsets.only(left: padding, right: padding), child: Text( text, - style: TextStyle( - color: primary ? Theme.of(context).colorScheme.onPrimary : null), + style: TextStyle(color: primary ? Theme.of(context).colorScheme.onPrimary : null), ), ), onPressed: () { diff --git a/lib/src/components/sp_compound_icon.dart b/lib/src/components/sp_compound_icon.dart index 4cb5408..22132df 100644 --- a/lib/src/components/sp_compound_icon.dart +++ b/lib/src/components/sp_compound_icon.dart @@ -43,10 +43,8 @@ class CompoundIcon extends StatelessWidget { secondIcon, size: size * .55, ), - decoration: IconDecoration( - border: IconBorder( - color: Theme.of(context).colorScheme.background, - width: size / 8)), + decoration: + IconDecoration(border: IconBorder(color: Theme.of(context).colorScheme.background, width: size / 8)), ), ), ], diff --git a/lib/src/components/sp_price_text.dart b/lib/src/components/sp_price_text.dart index 65532d9..17bfaf5 100644 --- a/lib/src/components/sp_price_text.dart +++ b/lib/src/components/sp_price_text.dart @@ -37,13 +37,10 @@ class SPPriceText extends StatelessWidget { ), WidgetSpan( child: Transform.translate( - offset: Offset( - 1.5, -DefaultTextStyle.of(context).style.fontSize! * 4 / 7), + offset: Offset(1.5, -DefaultTextStyle.of(context).style.fontSize! * 4 / 7), child: Text( superscript, - style: TextStyle( - fontSize: - DefaultTextStyle.of(context).style.fontSize! * 5 / 7), + style: TextStyle(fontSize: DefaultTextStyle.of(context).style.fontSize! * 5 / 7), ), ), ), diff --git a/lib/src/listview/item_list_model.dart b/lib/src/listview/item_list_model.dart index 158d1d2..8dae44a 100644 --- a/lib/src/listview/item_list_model.dart +++ b/lib/src/listview/item_list_model.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:spritverbrauch/src/utils/sqlite_service.dart'; class ItemListModel extends ChangeNotifier { - /// Internal, private state of the cart. + List _items = []; int _hiddenEntries = 0; - - /// An unmodifiable view of the items in the cart. + + UnmodifiableListView get items => UnmodifiableListView(_items); int get hiddenEntries => _hiddenEntries; @@ -16,7 +16,7 @@ class ItemListModel extends ChangeNotifier { final sqlitesevice = SqliteService(); final list = await sqlitesevice.getItems(); _items = list; - // This call tells the widgets that are listening to this model to rebuild. + notifyListeners(); } @@ -26,25 +26,25 @@ class ItemListModel extends ChangeNotifier { final listFiltered = await sqlitesevice.getItemsFiltered(start ?? DateTime(1970), end ?? DateTime.now()); _items = listFiltered; _hiddenEntries = list.length - listFiltered.length; - // This call tells the widgets that are listening to this model to rebuild. + notifyListeners(); } - /// Adds [item] to the list. + void add(ListItem item) { final sqlitesevice = SqliteService(); sqlitesevice.createItem(item); _items.add(item); - // This call tells the widgets that are listening to this model to rebuild. + notifyListeners(); } - /// Removes an items from the list. + void remove(ListItem item) { final sqlitesevice = SqliteService(); sqlitesevice.deleteItem(item.id); _items.remove(item); - // This call tells the widgets that are listening to this model to rebuild. + notifyListeners(); } } diff --git a/lib/src/listview/item_list_view.dart b/lib/src/listview/item_list_view.dart index 86479ca..ac2319f 100644 --- a/lib/src/listview/item_list_view.dart +++ b/lib/src/listview/item_list_view.dart @@ -13,8 +13,7 @@ class ItemListView extends StatefulWidget { class ItemListViewState extends State { @override Widget build(BuildContext context) { - return Consumer( - builder: (BuildContext context, ItemListModel value, Widget? child) { + return Consumer(builder: (BuildContext context, ItemListModel value, Widget? child) { var items = value.items; return ListView.separated( itemCount: items.length, diff --git a/lib/src/listview/list_item.dart b/lib/src/listview/list_item.dart index dbe968c..a51dfa2 100644 --- a/lib/src/listview/list_item.dart +++ b/lib/src/listview/list_item.dart @@ -40,8 +40,7 @@ class _ListEntryState extends State { var year = date.year.toString(); String locale = Intl.systemLocale; - var formatter = - NumberFormat.decimalPatternDigits(decimalDigits: 2, locale: locale); + var formatter = NumberFormat.decimalPatternDigits(decimalDigits: 2, locale: locale); var litersPerKilometer = formatter.format(widget.item.litersPerKilometer); @@ -54,134 +53,129 @@ class _ListEntryState extends State { var pricePerLiter = widget.item.pricePerLiter; return DefaultTextStyle( - style: TextStyle( - fontSize: 16, color: Theme.of(context).colorScheme.onBackground), - child: - InkWell( - onLongPress: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Eintrag löschen?'), - content: const Text('Diesen Eintrag wirklich löschen?'), - actions: [ - TextButton( - child: const Text('Abbrechen'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: const Text('Bestätigen'), - onPressed: () { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( - content: Text("Eintrag entfernt"), - showCloseIcon: true, - )); - - Provider.of(context, listen: false) - .remove(item); - - Navigator.of(context).pop(); - }, - ), - ], + style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onBackground), + child: InkWell( + onLongPress: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Eintrag löschen?'), + content: const Text('Diesen Eintrag wirklich löschen?'), + actions: [ + TextButton( + child: const Text('Abbrechen'), + onPressed: () { + Navigator.of(context).pop(); + }, ), - ); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4, horizontal: 15), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 7), - child: Row( + TextButton( + child: const Text('Bestätigen'), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Eintrag entfernt"), + showCloseIcon: true, + )); + + Provider.of(context, listen: false).remove(item); + + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 15), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 7), + child: Row( + children: [ + Expanded( + child: Row( children: [ - Expanded( - child: Row( - children: [ - const Icon(Icons.date_range_outlined), - const SizedBox(width: 2), - Text("$day.$month.$year"), - ], - )), - Expanded( - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const CompoundIcon( - firstIcon: Icons.local_gas_station_outlined, - secondIcon: Icons.route_outlined, - ), - const SizedBox(width: 2), - Text("$litersPerKilometer l/km"), - ], - ), - )), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.euro_outlined), - const SizedBox(width: 2), - SPPriceText(value: price, unit: '€'), - ], - ), - )), + const Icon(Icons.date_range_rounded), + const SizedBox(width: 2), + Text("$day.$month.$year"), ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 7), - child: Row( + )), + Expanded( + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const CompoundIcon( + firstIcon: Icons.local_gas_station_rounded, + secondIcon: Icons.route_rounded, + ), + const SizedBox(width: 2), + Text("$litersPerKilometer l/km"), + ], + ), + )), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.euro_rounded), + const SizedBox(width: 2), + SPPriceText(value: price, unit: '€'), + ], + ), + )), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 7), + child: Row( + children: [ + Expanded( + child: Row( children: [ - Expanded( - child: Row( + const Icon(Icons.route_rounded), + const SizedBox(width: 2), + Text("$distance km"), + ], + )), + Expanded( + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.local_gas_station_rounded), + const SizedBox(width: 2), + Text("$fuel l"), + ], + ), + )), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.route_outlined), + const CompoundIcon( + firstIcon: Icons.euro_rounded, + secondIcon: Icons.local_gas_station_rounded, + ), const SizedBox(width: 2), - Text("$distance km"), + SPPriceText(value: pricePerLiter, unit: '€'), ], - )), - Expanded( - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.local_gas_station_outlined), - const SizedBox(width: 2), - Text("$fuel l"), - ], - ), - )), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const CompoundIcon( - firstIcon: Icons.euro_outlined, - secondIcon: Icons.local_gas_station_outlined, - ), - const SizedBox(width: 2), - SPPriceText(value: pricePerLiter, unit: '€'), - ], - ), - ), ), - ], + ), ), - ), - ], + ], + ), ), - ), + ], ), + ), + ), ); } } diff --git a/lib/src/overview.dart b/lib/src/overview.dart index d1fcf68..6862498 100644 --- a/lib/src/overview.dart +++ b/lib/src/overview.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:spritverbrauch/src/components/sp_price_text.dart'; -import 'package:spritverbrauch/src/filter/filter_model.dart'; +import 'package:spritverbrauch/src/settings/filter_model.dart'; import 'package:spritverbrauch/src/components/sp_compound_icon.dart'; import 'package:spritverbrauch/src/listview/item_list_model.dart'; @@ -18,8 +18,8 @@ class Overview extends StatelessWidget { Widget build(BuildContext context) { Provider.of(context, listen: false).loadPreferences(); - return Consumer2(builder: (BuildContext context, - FilterModel filterModel, ItemListModel itemListModel, Widget? child) { + return Consumer2( + builder: (BuildContext context, FilterModel filterModel, ItemListModel itemListModel, Widget? child) { final filterEnabled = filterModel.filterEnabled; final dateFilter = filterModel.dateFilter; final startDateSingle = filterModel.startDateSingle; @@ -29,16 +29,13 @@ class Overview extends StatelessWidget { if (filterEnabled) { switch (dateFilter) { case DateFilter.fromDate: - Provider.of(context, listen: false) - .loadFiltered(start: startDateSingle); + Provider.of(context, listen: false).loadFiltered(start: startDateSingle); break; case DateFilter.dateRange: - Provider.of(context, listen: false) - .loadFiltered(start: startDate, end: endDate); + Provider.of(context, listen: false).loadFiltered(start: startDate, end: endDate); break; default: - Provider.of(context, listen: false) - .loadFiltered(start: startDateSingle); + Provider.of(context, listen: false).loadFiltered(start: startDateSingle); break; } } else { @@ -66,33 +63,25 @@ class Overview extends StatelessWidget { litersDB.add(element.fuelInLiters); } - litersPerKilometerDisplay = (litersPerKilometerDB.fold( - 0.0, (previousValue, element) => previousValue + element)) / - litersPerKilometerDB.length; + litersPerKilometerDisplay = + (litersPerKilometerDB.fold(0.0, (previousValue, element) => previousValue + element)) / + litersPerKilometerDB.length; - priceDisplay = (priceTotalDB.fold( - 0.0, (previousValue, element) => previousValue + element)) / - priceTotalDB.length; + priceDisplay = + (priceTotalDB.fold(0.0, (previousValue, element) => previousValue + element)) / priceTotalDB.length; - distanceDisplay = (distanceDB.fold( - 0.0, (previousValue, element) => previousValue + element)) / - distanceDB.length; + distanceDisplay = + (distanceDB.fold(0.0, (previousValue, element) => previousValue + element)) / distanceDB.length; - pricePerLiterDisplay = (priceTotalDB.fold( - 0.0, (previousValue, element) => previousValue + element)) / - (litersDB.fold( - 0.0, (previousValue, element) => previousValue + element)); + pricePerLiterDisplay = (priceTotalDB.fold(0.0, (previousValue, element) => previousValue + element)) / + (litersDB.fold(0.0, (previousValue, element) => previousValue + element)); - pricePerKilometerDisplay = (priceTotalDB.fold( - 0.0, (previousValue, element) => previousValue + element)) / - (distanceDB.fold( - 0.0, (previousValue, element) => previousValue + element)); + pricePerKilometerDisplay = (priceTotalDB.fold(0.0, (previousValue, element) => previousValue + element)) / + (distanceDB.fold(0.0, (previousValue, element) => previousValue + element)); } return DefaultTextStyle( - style: TextStyle( - fontSize: textSize, - color: Theme.of(context).colorScheme.onBackground), + style: TextStyle(fontSize: textSize, color: Theme.of(context).colorScheme.onBackground), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, @@ -102,8 +91,8 @@ class Overview extends StatelessWidget { value: litersPerKilometerDisplay, unit: 'L/km', icon: const CompoundIcon( - firstIcon: Icons.local_gas_station_outlined, - secondIcon: Icons.route_outlined, + firstIcon: Icons.local_gas_station_rounded, + secondIcon: Icons.route_rounded, size: iconSize, ), padding: padding, @@ -113,7 +102,7 @@ class Overview extends StatelessWidget { value: priceDisplay, unit: '€', icon: const Icon( - Icons.euro_outlined, + Icons.euro_rounded, size: iconSize, ), padding: padding, @@ -124,7 +113,7 @@ class Overview extends StatelessWidget { value: distanceDisplay, unit: 'km', icon: const Icon( - Icons.route_outlined, + Icons.route_rounded, size: iconSize, ), padding: padding, @@ -134,8 +123,8 @@ class Overview extends StatelessWidget { value: pricePerLiterDisplay, unit: '€/L', icon: const CompoundIcon( - firstIcon: Icons.local_gas_station_outlined, - secondIcon: Icons.euro_outlined, + firstIcon: Icons.local_gas_station_rounded, + secondIcon: Icons.euro_rounded, size: iconSize, ), padding: padding, @@ -146,8 +135,8 @@ class Overview extends StatelessWidget { value: pricePerKilometerDisplay, unit: '€/km', icon: const CompoundIcon( - firstIcon: Icons.route_outlined, - secondIcon: Icons.euro_outlined, + firstIcon: Icons.route_rounded, + secondIcon: Icons.euro_rounded, size: iconSize, ), padding: padding, @@ -182,8 +171,7 @@ class OverviewElement extends StatelessWidget { Widget build(BuildContext context) { String locale = Intl.systemLocale; - final formatter = - NumberFormat.decimalPatternDigits(decimalDigits: 2, locale: locale); + final formatter = NumberFormat.decimalPatternDigits(decimalDigits: 2, locale: locale); final textValue = formatter.format(value); diff --git a/lib/src/filter/filter.dart b/lib/src/settings/filter.dart similarity index 98% rename from lib/src/filter/filter.dart rename to lib/src/settings/filter.dart index b95b147..239cc9c 100644 --- a/lib/src/filter/filter.dart +++ b/lib/src/settings/filter.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import 'package:spritverbrauch/src/components/sp_button.dart'; -import 'package:spritverbrauch/src/filter/filter_model.dart'; +import 'package:spritverbrauch/src/settings/filter_model.dart'; class Filter extends StatefulWidget { const Filter({super.key}); @@ -59,7 +59,7 @@ class _FilterState extends State { Provider.of(context, listen: false).loadPreferences(); return Scaffold( - appBar: AppBar(title: const Text('Filter konfigurieren')), + appBar: AppBar(title: const Text('Filter konfigurieren'), centerTitle: true,), body: Padding( padding: const EdgeInsets.all(16), child: Consumer( diff --git a/lib/src/filter/filter_model.dart b/lib/src/settings/filter_model.dart similarity index 57% rename from lib/src/filter/filter_model.dart rename to lib/src/settings/filter_model.dart index 9855fc2..aadc1cf 100644 --- a/lib/src/filter/filter_model.dart +++ b/lib/src/settings/filter_model.dart @@ -11,7 +11,7 @@ const dateStartKey = 'dateStart'; const dateEndKey = 'dateEnd'; class FilterModel extends ChangeNotifier { - late SharedPreferences preferences; + late SharedPreferences _preferences; bool _filterEnabled = false; DateFilter _dateFilter = DateFilter.fromDate; @@ -28,24 +28,24 @@ class FilterModel extends ChangeNotifier { bool reset = false; void loadPreferences() async { - preferences = await SharedPreferences.getInstance(); + _preferences = await SharedPreferences.getInstance(); // Filter Toggle - var filterEnabledDb = preferences.getBool(filterEnabledKey); + var filterEnabledDb = _preferences.getBool(filterEnabledKey); if (filterEnabledDb == null) { filterEnabledDb = false; - preferences.setBool(filterEnabledKey, false); + _preferences.setBool(filterEnabledKey, false); } _filterEnabled = filterEnabledDb; // Filter State - var dateFilterDb = preferences.getString(dateFilterKey); + var dateFilterDb = _preferences.getString(dateFilterKey); if (dateFilterDb == null) { dateFilterDb = DateFilter.fromDate.toString(); - preferences.setString(dateFilterKey, DateFilter.fromDate.toString()); + _preferences.setString(dateFilterKey, DateFilter.fromDate.toString()); } switch (dateFilterDb) { @@ -58,27 +58,23 @@ class FilterModel extends ChangeNotifier { } // Value for fromDate Filter - var dateStartSingleDb = preferences.getInt(dateStartSingleKey); + var dateStartSingleDb = _preferences.getInt(dateStartSingleKey); - _startDateSingle = (dateStartSingleDb == null) - ? null - : DateTime.fromMillisecondsSinceEpoch(dateStartSingleDb); + _startDateSingle = (dateStartSingleDb == null) ? null : DateTime.fromMillisecondsSinceEpoch(dateStartSingleDb); // Start value for dateRange Filter - var dateStartDb = preferences.getInt(dateStartKey); + var dateStartDb = _preferences.getInt(dateStartKey); if (dateStartDb == null) { var sqlitesevice = SqliteService(); var list = await sqlitesevice.getItems(); - dateStartDb = (list.isNotEmpty) - ? list.last.date - : DateTime.now().millisecondsSinceEpoch; + dateStartDb = (list.isNotEmpty) ? list.last.date : DateTime.now().millisecondsSinceEpoch; } _startDate = DateTime.fromMillisecondsSinceEpoch(dateStartDb); // End value for dateRange Filter - var dateEndDb = preferences.getInt(dateEndKey); + var dateEndDb = _preferences.getInt(dateEndKey); dateEndDb ??= DateTime.now().millisecondsSinceEpoch; _endDate = DateTime.fromMillisecondsSinceEpoch(dateEndDb); @@ -86,63 +82,55 @@ class FilterModel extends ChangeNotifier { notifyListeners(); } - /// Adds [item] to the list. void setFilterEnabled(bool enabled) { _filterEnabled = enabled; - preferences.setBool(filterEnabledKey, enabled); + _preferences.setBool(filterEnabledKey, enabled); if (reset) loadPreferences(); - // This call tells the widgets that are listening to this model to rebuild. notifyListeners(); } - /// Removes an items from the list. void setDateFilter(DateFilter filter) { _dateFilter = filter; - preferences.setString(dateFilterKey, filter.toString()); + _preferences.setString(dateFilterKey, filter.toString()); - // This call tells the widgets that are listening to this model to rebuild. notifyListeners(); } void setStartDateSingle(DateTime date) { _startDateSingle = date; - preferences.setInt(dateStartSingleKey, date.millisecondsSinceEpoch); + _preferences.setInt(dateStartSingleKey, date.millisecondsSinceEpoch); - // This call tells the widgets that are listening to this model to rebuild. notifyListeners(); } void setStartDate(DateTime date) { _startDate = date; - preferences.setInt(dateStartKey, date.millisecondsSinceEpoch); + _preferences.setInt(dateStartKey, date.millisecondsSinceEpoch); - // This call tells the widgets that are listening to this model to rebuild. notifyListeners(); } void setEndDate(DateTime date) { _endDate = date; - preferences.setInt(dateEndKey, date.millisecondsSinceEpoch); + _preferences.setInt(dateEndKey, date.millisecondsSinceEpoch); - // This call tells the widgets that are listening to this model to rebuild. notifyListeners(); } void resetFilter() { - preferences.setBool(filterEnabledKey, false); - preferences.setString(dateFilterKey, DateFilter.fromDate.toString()); - preferences.remove(dateStartSingleKey); - preferences.remove(dateStartKey); - preferences.remove(dateEndKey); + _preferences.setBool(filterEnabledKey, false); + _preferences.setString(dateFilterKey, DateFilter.fromDate.toString()); + _preferences.remove(dateStartSingleKey); + _preferences.remove(dateStartKey); + _preferences.remove(dateEndKey); _filterEnabled = false; _dateFilter = DateFilter.fromDate; reset = true; - // This call tells the widgets that are listening to this model to rebuild. notifyListeners(); } } diff --git a/lib/src/settings/pages/about.dart b/lib/src/settings/pages/about.dart new file mode 100644 index 0000000..b9f8267 --- /dev/null +++ b/lib/src/settings/pages/about.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:spritverbrauch/src/components/font_awesome.dart'; +import 'package:spritverbrauch/src/components/settings/settings_group.dart'; +import 'package:spritverbrauch/src/components/settings/settings_item.dart'; +import 'package:spritverbrauch/src/components/settings/settings_topic_page.dart'; +import 'package:spritverbrauch/src/utils/url_launcher.dart'; + +class About extends StatefulWidget { + About({super.key}); + + @override + State createState() => _AboutState(); +} + +class _AboutState extends State { + String appName = ''; + String version = ''; + String buildNumber = ''; + + @override + void initState() { + super.initState(); + asyncInitState(); + } + + void asyncInitState() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + setState(() { + appName = packageInfo.appName; + version = packageInfo.version; + buildNumber = packageInfo.buildNumber; + }); + } + + @override + Widget build(BuildContext context) { + return SettingsTopicPage(title: 'Informationen', children: [ + SettingsItem( + icon: Icons.local_gas_station_rounded, + title: appName, + subtitle: 'Version $version ($buildNumber)', + onTap: () { + Uri url = Uri.parse('https://github.com/RedCommander735/Spritverbrauch/releases/'); + launchURL(url); + }, + ), + SettingsGroup(title: 'General', children: [ + SettingsItem( + icon: FontAwesomeBrands.github, + title: 'GitHub repository', + subtitle: 'https://github.com/RedCommander735/\nSpritverbrauch', + onTap: () { + Uri url = Uri.parse('https://github.com/RedCommander735/Spritverbrauch/'); + launchURL(url); + }, + ), + SettingsItem( + icon: FontAwesomeBrands.github, + title: 'License', + subtitle: 'GPL-3.0', + onTap: () { + Uri url = Uri.parse('https://github.com/RedCommander735/Spritverbrauch/blob/main/LICENSE'); + launchURL(url); + }, + ), + SettingsItem( + icon: Icons.extension_rounded, + title: 'Libraries', + subtitle: 'Eine Liste aller verwendeten Bibliotheken', + onTap: () { + showLicensePage( + context: context, + applicationName: appName, + applicationVersion: '$version ($buildNumber)', + applicationLegalese: 'Copyright (C) 2024 RedCommander735'); + }, + ), + ]) + ]); + } +} diff --git a/lib/src/settings/pages/general.dart b/lib/src/settings/pages/general.dart new file mode 100644 index 0000000..bc36001 --- /dev/null +++ b/lib/src/settings/pages/general.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:spritverbrauch/src/components/font_awesome.dart'; +import 'package:spritverbrauch/src/components/settings/settings_group.dart'; +import 'package:spritverbrauch/src/components/settings/settings_item.dart'; +import 'package:spritverbrauch/src/components/settings/settings_topic_page.dart'; +import 'package:spritverbrauch/src/utils/csv_handler.dart'; +import 'package:spritverbrauch/src/utils/url_launcher.dart'; + +class General extends StatefulWidget { + General({super.key}); + + @override + State createState() => _GeneralState(); +} + +class _GeneralState extends State { + String appName = ''; + String version = ''; + String buildNumber = ''; + + @override + void initState() { + super.initState(); + asyncInitState(); + } + + void asyncInitState() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + setState(() { + appName = packageInfo.appName; + version = packageInfo.version; + buildNumber = packageInfo.buildNumber; + }); + } + + @override + Widget build(BuildContext context) { + return SettingsTopicPage(title: 'Informationen', children: [ + SettingsGroup(title: 'Backup', children: [ + SettingsItem( + icon: Icons.save_rounded, + title: 'Backup', + subtitle: 'Als csv speichern', + onTap: () async { + final backupstorage = BackupStorage(); + bool success = await backupstorage.writeBackup(); + + if (!success) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Zum speichern von Backups muss Zugriff auf Dateien gewährt sein."), + showCloseIcon: true, + )); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Datei erfolgreich im Downloads-Ordner gespeichert."), + showCloseIcon: true, + )); + } + }, + ), + SettingsItem( + icon: Icons.upload_file_rounded, + title: 'Laden', + subtitle: 'Aus csv Datei laden', + onTap: () async { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Backup laden?'), + content: const Text( + 'Das Laden des Backups wird ein Backup aller zur Zeit gespeicherten Einträge machen, diese dann löschen und das Backup aus der Datei laden.'), + actions: [ + TextButton( + child: const Text('Abbrechen'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Bestätigen'), + onPressed: () async { + // TODO Implement file explorer + final backupstorage = BackupStorage(); + bool success = await backupstorage.readBackup(); + + if (!success) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Beim lesen der Datei ist ein Fehler aufgetreten."), + showCloseIcon: true, + )); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Das Backup wurde erfolgreich geladen."), + showCloseIcon: true, + )); + } + + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + ), + ]) + ]); + } +} diff --git a/lib/src/settings/settings.dart b/lib/src/settings/settings.dart new file mode 100644 index 0000000..b331f7f --- /dev/null +++ b/lib/src/settings/settings.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:spritverbrauch/src/components/settings/settings_topic.dart'; + +import 'package:spritverbrauch/src/settings/filter_model.dart'; +import 'package:spritverbrauch/src/settings/pages/about.dart'; +import 'package:spritverbrauch/src/settings/pages/general.dart'; + +class Settings extends StatefulWidget { + const Settings({super.key, this.textSize = 18}); + + final double textSize; + + @override + State createState() => _SettingsState(); +} + +class _SettingsState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + Provider.of(context, listen: false).loadPreferences(); + + return Scaffold( + appBar: AppBar( + title: const Text('Einstellungen'), + centerTitle: true, + ), + body: Consumer( + builder: (BuildContext context, FilterModel filterModel, Widget? child) { + return DefaultTextStyle( + style: TextStyle(fontSize: widget.textSize, color: Theme.of(context).colorScheme.onBackground), + child: Column( + children: [ + SettingsTopic( + icon: Icons.settings_rounded, + title: 'Allgemein', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => General(), + ), + ); + }, + ), + SettingsTopic( + icon: Icons.info_outline, + title: 'Informationen', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => About(), + ), + ); + }, + ) + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/src/settings/settings_model.dart b/lib/src/settings/settings_model.dart new file mode 100644 index 0000000..7368ad8 --- /dev/null +++ b/lib/src/settings/settings_model.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class SettingsModel extends ChangeNotifier { + // late SharedPreferences _preferences; + + bool _filterEnabled = false; + + bool get filterEnabled => _filterEnabled; + + bool reset = false; +} diff --git a/lib/src/utils/csv_handler.dart b/lib/src/utils/csv_handler.dart new file mode 100644 index 0000000..50be857 --- /dev/null +++ b/lib/src/utils/csv_handler.dart @@ -0,0 +1,148 @@ +import 'dart:io'; + +import 'package:csv/csv.dart'; +import 'package:spritverbrauch/src/utils/sqlite_service.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class BackupStorage { + Future get _localPath async { + + Directory dir = Directory('/storage/emulated/0/Download/spritverbrauch'); + dir.create(); + + print(dir.path); + + return dir.path; + } + + Future get _localFile async { + final path = await _localPath; + return File('$path/backup.csv'); + } + + Future get _localBackupFile async { + final path = await _localPath; + return File('$path/backup-before-load.csv'); + } + + Future readBackup() async { + writeBackup(beforeLoad: true); + + try { + final file = await _localFile; + + late String contents; + // Read the file + var status = await Permission.manageExternalStorage.status; + + if (status.isPermanentlyDenied) { + return false; + } + if (status.isDenied) { + status = await Permission.manageExternalStorage.request(); + if (status.isPermanentlyDenied) { + return false; + } else if (status.isGranted) { + contents = await file.readAsString(); + } + } + if (status.isGranted) { + contents = await file.readAsString(); + } + + if (contents.isEmpty) { + return false; + } + + CsvMapConverter converter = CsvMapConverter(); + SqliteService sqlite = SqliteService(); + + List> itemMaps = converter.convertCsvToMaps(contents); + + var listItems = itemMaps.map((e) => ListItem.fromMap(e)).toList(); + + sqlite.deleteAll(); + + sqlite.createItems(listItems); + + return true; + + } catch (e) { + // If encountering an error, return 0 + return false; + } + } + + Future writeBackup({bool beforeLoad = false}) async { + final file = (beforeLoad) ? await _localBackupFile : await _localFile; + + CsvMapConverter converter = CsvMapConverter(); + SqliteService sqlite = SqliteService(); + + late String csv; + + final maps = await sqlite.getItemsAsMap(); + if (maps.isNotEmpty) { + csv = converter.convertMapsToCsv(maps); + } else { + csv = ''; + } + + // Write the file + var status = await Permission.manageExternalStorage.status; + + if (status.isPermanentlyDenied) { + return false; + } + if (status.isDenied) { + status = await Permission.manageExternalStorage.request(); + if (status.isPermanentlyDenied) { + return false; + } else if (status.isGranted) { + file.writeAsString(csv); + return true; + } + } + if (status.isGranted) { + file.writeAsString(csv); + return true; + } + return false; + } +} + +class CsvMapConverter { + late CsvToListConverter csvToListConverter; + late ListToCsvConverter listToCsvConverter; + + CsvMapConverter() { + csvToListConverter = const CsvToListConverter(); + listToCsvConverter = const ListToCsvConverter(); + } + + List> convertCsvToMaps(String csv) { + List> list = csvToListConverter.convert(csv); + List legend = list[0]; + List> maps = []; + list.sublist(1).forEach((List l) { + Map map = {}; + for (int i = 0; i < legend.length; i++) { + map.putIfAbsent('${legend[i]}', () => l[i]); + } + maps.add(map); + }); + return maps; + } + + String convertMapsToCsv(List> maps) { + List legend = maps[0].keys.toList(); + List> lists = [legend]; + for (var m in maps) { + List list = m.values.toList(); + lists.add(list); + } + + String csv = listToCsvConverter.convert(lists); + return csv; + } +} \ No newline at end of file diff --git a/lib/src/utils/licenses.dart b/lib/src/utils/licenses.dart new file mode 100644 index 0000000..05ed0f2 --- /dev/null +++ b/lib/src/utils/licenses.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; + +void addLicenses() { + LicenseRegistry.addLicense(() => Stream.value( + const LicenseEntryWithLineBreaks( + ['Font Awesome Free'], + ''' +Font Awesome Free License + +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.**''', + ), + )); +} diff --git a/lib/src/utils/number_formatter.dart b/lib/src/utils/number_formatter.dart index 1691872..d9ef2eb 100644 --- a/lib/src/utils/number_formatter.dart +++ b/lib/src/utils/number_formatter.dart @@ -47,7 +47,11 @@ class FormattedDouble { @override String toString({bool roundLastDigit = true, int? fractionDigits}) { - return '$_integer$_decimalSeperator${fractionalPartAsInt(roundLastDigit: roundLastDigit, fractionDigits: fractionDigits)}'; + if (fractionDigits != null) { + return '$_integer$_decimalSeperator${fractionalPartAsIntToString(roundLastDigit: roundLastDigit, fractionDigits: fractionDigits).padRight(fractionDigits, '0')}'; + } + + return '$_integer$_decimalSeperator${fractionalPartAsIntToString(roundLastDigit: roundLastDigit, fractionDigits: fractionDigits)}'; } String integerPartToString() { @@ -76,15 +80,10 @@ class FormattedDouble { if (fractionDigits != null && roundLastDigit) { fractionalString = _value.toStringAsFixed(fractionDigits).split('.').last.trimCharRight('0'); - } else if (fractionDigits != null && - fractionDigits > 0 && - !roundLastDigit) { - String fracPart = _value.toString().split('.').last; - fractionalString = - (fracPart.length > fractionDigits) ? fracPart.substring(0, fractionDigits) : fracPart; - } else if (fractionDigits != null && - fractionDigits == 0 && - !roundLastDigit) { + } else if (fractionDigits != null && fractionDigits > 0 && !roundLastDigit) { + String fracPart = _value.toString().split('.').last; + fractionalString = (fracPart.length > fractionDigits) ? fracPart.substring(0, fractionDigits) : fracPart; + } else if (fractionDigits != null && fractionDigits == 0 && !roundLastDigit) { return null; } else { fractionalString = _value.toString().split('.').last; @@ -121,7 +120,6 @@ class NegativeValue implements Exception { } } - extension StringFuncs on String { String trimCharLeft(String pattern) { if (isEmpty || pattern.isEmpty || pattern.length > length) return this; @@ -144,4 +142,4 @@ extension StringFuncs on String { String trimChar(String pattern) { return trimCharLeft(pattern).trimCharRight(pattern); } -} \ No newline at end of file +} diff --git a/lib/src/utils/sqlite_service.dart b/lib/src/utils/sqlite_service.dart index 79c03b3..375cd4c 100644 --- a/lib/src/utils/sqlite_service.dart +++ b/lib/src/utils/sqlite_service.dart @@ -3,7 +3,7 @@ import 'package:path/path.dart'; import 'package:flutter/material.dart'; class SqliteService { - Future initDB() async { + Future _initDB() async { final database = openDatabase( join(await getDatabasesPath(), 'fuel_usage.db'), onCreate: (db, version) { @@ -17,25 +17,39 @@ class SqliteService { } Future createItem(ListItem entity) async { - final Database db = await initDB(); - final id = await db.insert('fuel_usage', entity.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace); + final Database db = await _initDB(); + final id = await db.insert('fuel_usage', entity.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); return id; } + Future> createItems(List entitys) async { + final Database db = await _initDB(); + List ids = []; + for (var entity in entitys) { + int id = await db.insert('fuel_usage', entity.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); + ids.add(id); + } + return ids; + } + Future> getItems() async { - final db = await initDB(); - final List> queryResult = - await db.query('fuel_usage', orderBy: 'date'); + final db = await _initDB(); + final List> queryResult = await db.query('fuel_usage', orderBy: 'date'); final list = queryResult.map((e) => ListItem.fromMap(e)).toList(); return list.reversed.toList(); } + Future>> getItemsAsMap() async { + final db = await _initDB(); + final List> queryResult = await db.query('fuel_usage', orderBy: 'date'); + return queryResult.reversed.toList(); + } + Future> getItemsFiltered(DateTime startingDate, DateTime endDate) async { final start = startingDate.millisecondsSinceEpoch; final end = endDate.millisecondsSinceEpoch; - final db = await initDB(); + final db = await _initDB(); final List> queryResult = await db.query('fuel_usage', orderBy: 'date', where: 'date >= ? AND date <= ?', whereArgs: [start, end]); final list = queryResult.map((e) => ListItem.fromMap(e)).toList(); @@ -43,13 +57,22 @@ class SqliteService { } Future deleteItem(int id) async { - final db = await initDB(); + final db = await _initDB(); try { await db.delete("fuel_usage", where: "id = ?", whereArgs: [id]); } catch (err) { debugPrint("Something went wrong when deleting an item: $err"); } } + + Future deleteAll() async { + final db = await _initDB(); + try { + await db.delete("fuel_usage"); + } catch (err) { + debugPrint("Something went wrong when deleting an item: $err"); + } + } } class ListItem { diff --git a/lib/src/utils/url_launcher.dart b/lib/src/utils/url_launcher.dart new file mode 100644 index 0000000..8bf30b2 --- /dev/null +++ b/lib/src/utils/url_launcher.dart @@ -0,0 +1,7 @@ +import 'package:url_launcher/url_launcher.dart'; + +Future launchURL(Uri url) async { + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 675b719..fe56f8d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 3e303c1..1836621 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c72a861..1dd30f4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,11 +6,17 @@ import FlutterMacOS import Foundation import dynamic_color +import package_info_plus +import path_provider_foundation import shared_preferences_foundation import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index bc517c3..0c72c99 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,14 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" - cupertino_icons: + csv: dependency: "direct main" description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + name: csv + sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "6.0.0" dynamic_color: dependency: "direct main" description: @@ -112,6 +112,22 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" husky: dependency: "direct dev" description: @@ -208,6 +224,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: "direct main" description: @@ -216,6 +248,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -240,6 +296,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.dev" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5" + url: "https://pub.dev" + source: hosted + version: "12.0.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + url: "https://pub.dev" + source: hosted + version: "9.4.4" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" platform: dependency: transitive description: @@ -397,6 +501,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.dev" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c8a3db9..0575014 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # 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.1.2+1 +version: 1.2.0+3 environment: flutter: '3.19.0' @@ -32,10 +32,6 @@ 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.6 icon_decoration: ^2.0.2 json_annotation: ^4.8.1 dynamic_color: ^1.7.0 @@ -44,6 +40,11 @@ dependencies: provider: ^6.1.2 intl: ^0.19.0 shared_preferences: ^2.2.3 + package_info_plus: ^8.0.0 + url_launcher: ^6.2.6 + path_provider: ^2.1.3 + csv: ^6.0.0 + permission_handler: ^11.3.1 dev_dependencies: flutter_test: @@ -68,6 +69,11 @@ flutter: # the material Icons class. uses-material-design: true + fonts: + - family: FontAwesomeBrands + fonts: + - asset: fonts/otfs/Font-Awesome-5-Brands-Regular-400.otf + weight: 400 # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg @@ -98,3 +104,4 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e4899a6..4266b12 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 841e8c4..2a2380c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,8 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + permission_handler_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST