From 715cc13d2bc44046bfac9e3cf10e0a5d262fa258 Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 15 Jun 2023 17:44:27 +0200 Subject: [PATCH 01/35] add dropdown setting --- lib/components/settings_widgets.dart | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/components/settings_widgets.dart b/lib/components/settings_widgets.dart index 975a5433..8901d09c 100644 --- a/lib/components/settings_widgets.dart +++ b/lib/components/settings_widgets.dart @@ -340,6 +340,43 @@ class _InputSettingsTileState extends State { } } +class DropDownSettingsTile extends StatelessWidget { + final Widget title; + final Widget? leading; + final Widget? description; + final bool disabled; + + final T value; + final List> items; + final void Function(T? value) onChanged; + + + const DropDownSettingsTile({required this.title, required this.value, + required this.onChanged, required this.items, this.disabled = false, this.leading, this.description, super.key}); + + @override + Widget build(BuildContext context) { + return SettingsTile( + title: title, + description: description, + leading: leading, + disabled: disabled, + onPressed: (BuildContext context) { }, + trailing: Row( + children: [ + DropdownButton( + value: value, + items: items, + onChanged: onChanged, + ), + const SizedBox(width: 15,) + ], + ), + ); + } + +} + class SettingsSection extends StatelessWidget { final Widget title; final List children; From a80780b2df64a9c134f38a855e8f91866e8c4c6f Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 15 Jun 2023 18:11:36 +0200 Subject: [PATCH 02/35] use dropdown for theme --- lib/screens/settings.dart | 45 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 65de33ae..4845ba2a 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -47,28 +47,33 @@ class SettingsPage extends StatelessWidget { ); }, ), - SwitchSettingsTile( - key: const Key('followSystemDarkMode'), - initialValue: settings.followSystemDarkMode, - onToggle: (value) { - settings.followSystemDarkMode = value; - }, - leading: const Icon(Icons.auto_mode), - title: Text(AppLocalizations.of(context)!.followSystemDarkMode)), - SwitchSettingsTile( - key: const Key('darkMode'), - initialValue: (() { - if (settings.followSystemDarkMode) { - return MediaQuery.of(context).platformBrightness == Brightness.dark; + DropDownSettingsTile( + key: const Key('thema'), + leading: const Icon(Icons.brightness_4), + title: Text(AppLocalizations.of(context)!.theme), + value: settings.followSystemDarkMode ? 0 : (settings.darkMode ? 1 : 2), + items: [ + DropdownMenuItem(value: 0, child: Text(AppLocalizations.of(context)!.system)), + DropdownMenuItem(value: 1, child: Text(AppLocalizations.of(context)!.dark)), + DropdownMenuItem(value: 2, child: Text(AppLocalizations.of(context)!.light)) + ], + onChanged: (int? value) { + switch (value) { + case 0: + settings.followSystemDarkMode = true; + break; + case 1: + settings.followSystemDarkMode = false; + settings.darkMode = true; + break; + case 2: + settings.followSystemDarkMode = false; + settings.darkMode = false; + break; + default: + assert(false); } - return settings.darkMode; - })(), - onToggle: (value) { - settings.darkMode = value; }, - leading: const Icon(Icons.dark_mode), - title: Text(AppLocalizations.of(context)!.darkMode), - disabled: settings.followSystemDarkMode, ), SliderSettingsTile( key: const Key('iconSize'), From 0e4d797fe2e874eba4eba715a6cc2f9eaefa0573 Mon Sep 17 00:00:00 2001 From: derdilla Date: Fri, 16 Jun 2023 15:55:38 +0200 Subject: [PATCH 03/35] add structure for export page --- lib/l10n/app_de.arb | 10 +++- lib/l10n/app_en.arb | 10 +++- lib/model/export_import.dart | 41 ++++++++++++++++ lib/model/settings_store.dart | 9 ++++ .../subsettings/export_import_screen.dart | 47 +++++++++++++++++++ 5 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 lib/model/export_import.dart create mode 100644 lib/screens/subsettings/export_import_screen.dart diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 79defc16..15c2b2ff 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -52,8 +52,10 @@ "layout": "Layout", "allowManualTimeInput": "Editierbare Zeitangaben", "enterTimeFormatScreen": "Datums-/Zeitformat", - "followSystemDarkMode": "Thema wie System", - "darkMode": "Dunkles Thema", + "theme": "Thema", + "system": "System", + "dark": "Dunkel", + "light": "Hell", "iconSize": "Größe der Knöpfe", "graphLineThickness": "Linienstärke d. Graphen", "animationSpeed": "Animationsdauer", @@ -73,6 +75,10 @@ "sysWarn": "Warnwert Sys", "diaWarn": "Warnwert Dia", "data": "Daten", + "exportImport": "Exportieren / Importieren", + "exportFormat": "Exportformat", + "csv": "csv", + "pdf": "pdf", "useExportCompatability": "Kompatibler Export", "useExportCompatabilityDesc": "Signalisiert Export als Text", "export": "Exportieren", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9a875bd6..a70d1269 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -52,8 +52,10 @@ "layout": "layout", "allowManualTimeInput": "allow manual time input", "enterTimeFormatScreen": "time format", - "followSystemDarkMode": "follow system dark mode", - "darkMode": "enable dark mode", + "theme": "theme", + "system": "System", + "dark": "dark", + "light": "light", "iconSize": "icon size", "graphLineThickness": "line thickness", "animationSpeed": "animation duration", @@ -73,6 +75,10 @@ "sysWarn": "systolic warn", "diaWarn": "diastolic warn", "data": "data", + "exportImport": "export / import", + "exportFormat": "export format", + "csv": "csv", + "pdf": "pdf", "useExportCompatability": "compatability export", "useExportCompatabilityDesc": "sets export mime type to text", "export": "export", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart new file mode 100644 index 00000000..b076ad91 --- /dev/null +++ b/lib/model/export_import.dart @@ -0,0 +1,41 @@ + +import 'dart:typed_data'; + +import 'package:intl/intl.dart'; + +import 'blood_pressure.dart'; + +class CSVExportSettings { + final DateFormat dateFormatter; + final String? fieldDelimiter; + final String? textDelimiter; + final String? textEndDelimiter; + + CSVExportSettings(this.dateFormatter, this.fieldDelimiter, this.textDelimiter, this.textEndDelimiter); +} + +class ExportFormat { + final int code; + + ExportFormat(this.code) { + if (code < 0 || code > 1) throw const FormatException('Not a export format'); + } + static ExportFormat csv = ExportFormat(0); + static ExportFormat pdf = ExportFormat(1); + + @override + bool operator == (Object other) { + try { + return code == (other as ExportFormat).code; + } on Exception { + try { + return code == (other as int); + } on Exception { + return false; + } + } + } + + @override + int get hashCode => code; +} \ No newline at end of file diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 0acae3d4..25875d90 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -1,4 +1,5 @@ import 'package:blood_pressure_app/model/blood_pressure.dart'; +import 'package:blood_pressure_app/model/export_import.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -279,6 +280,14 @@ class Settings extends ChangeNotifier { _prefs.setInt('titlesCount', newCount); notifyListeners(); } + + ExportFormat get exportFormat { + return ExportFormat(_prefs.getInt('exportFormat') ?? 0); + } + set exportFormat(ExportFormat format) { + _prefs.setInt('exportFormat', format.code); + notifyListeners(); + } } class TimeStep { diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart new file mode 100644 index 00000000..1f801318 --- /dev/null +++ b/lib/screens/subsettings/export_import_screen.dart @@ -0,0 +1,47 @@ + +import 'package:blood_pressure_app/components/settings_widgets.dart'; +import 'package:blood_pressure_app/model/export_import.dart'; +import 'package:blood_pressure_app/model/settings_store.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +class ExportImportScreen extends StatelessWidget { + const ExportImportScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(AppLocalizations.of(context)!.exportImport),), + body: Consumer(builder: (context, settings, child) { + List modeSpecificSettings = []; + if (settings.exportFormat == ExportFormat.csv) { + modeSpecificSettings = []; + } + + List options = [ + DropDownSettingsTile( + key: const Key('exportFormat'), + leading: const Icon(Icons.error), + title: Text(AppLocalizations.of(context)!.exportFormat), + value: settings.exportFormat, + items: [ + DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), + DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + ], + onChanged: (ExportFormat? value) { + if (value != null) { + settings.exportFormat = value; + } + }, + ) + ]; + options.addAll(modeSpecificSettings); + + return ListView( + children: options, + ); + }), + ); + } +} From 4daaec147f42c854c64abcbd79cee4d4c9269937 Mon Sep 17 00:00:00 2001 From: derdilla Date: Sat, 17 Jun 2023 00:21:26 +0200 Subject: [PATCH 04/35] add export page with advanced settings --- lib/l10n/app_de.arb | 9 ++- lib/l10n/app_en.arb | 9 ++- lib/model/export_import.dart | 7 +- lib/model/ram_only_implementations.dart | 11 ++++ lib/model/settings_store.dart | 66 ++++++++++++++++--- lib/screens/settings.dart | 22 ++++--- .../subsettings/export_import_screen.dart | 46 ++++++++++++- 7 files changed, 138 insertions(+), 32 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 15c2b2ff..71a9c34b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -77,8 +77,13 @@ "data": "Daten", "exportImport": "Exportieren / Importieren", "exportFormat": "Exportformat", - "csv": "csv", - "pdf": "pdf", + "exportMimeType": "Export MIME typ", + "csv": "CSV", + "pdf": "PDF", + "text": "Text", + "other": "Anderes", + "fieldDelimiter": "Feld separator", + "textDelimiter": "Textbegrenzung", "useExportCompatability": "Kompatibler Export", "useExportCompatabilityDesc": "Signalisiert Export als Text", "export": "Exportieren", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a70d1269..6ac588ac 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -77,8 +77,13 @@ "data": "data", "exportImport": "export / import", "exportFormat": "export format", - "csv": "csv", - "pdf": "pdf", + "exportMimeType": "export MIME type", + "csv": "CSV", + "pdf": "PDF", + "text": "text", + "other": "other", + "fieldDelimiter": "field delimiter", + "textDelimiter": "text delimiter", "useExportCompatability": "compatability export", "useExportCompatabilityDesc": "sets export mime type to text", "export": "export", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index b076ad91..8aaa6f25 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -1,17 +1,12 @@ -import 'dart:typed_data'; - import 'package:intl/intl.dart'; -import 'blood_pressure.dart'; - class CSVExportSettings { final DateFormat dateFormatter; final String? fieldDelimiter; final String? textDelimiter; - final String? textEndDelimiter; - CSVExportSettings(this.dateFormatter, this.fieldDelimiter, this.textDelimiter, this.textEndDelimiter); + CSVExportSettings(this.dateFormatter, this.fieldDelimiter, this.textDelimiter); } class ExportFormat { diff --git a/lib/model/ram_only_implementations.dart b/lib/model/ram_only_implementations.dart index b80dbb27..5fe2fdec 100644 --- a/lib/model/ram_only_implementations.dart +++ b/lib/model/ram_only_implementations.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:blood_pressure_app/model/blood_pressure.dart'; +import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:flutter/material.dart'; @@ -113,6 +114,7 @@ class RamSettings extends ChangeNotifier implements Settings { bool _useExportCompatability = false; bool _validateInputs = true; int _graphTitlesCount = 5; + ExportFormat _exportFormat = ExportFormat.csv; RamSettings() { _accentColor = createMaterialColor(0xFF009688); @@ -333,6 +335,15 @@ class RamSettings extends ChangeNotifier implements Settings { notifyListeners(); } + @override + ExportFormat get exportFormat => _exportFormat; + + @override + set exportFormat(ExportFormat value) { + _exportFormat = value; + notifyListeners(); + } + @override void changeStepSize(int value) { graphStepSize = value; diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 25875d90..2adfdf2d 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -1,5 +1,6 @@ import 'package:blood_pressure_app/model/blood_pressure.dart'; import 'package:blood_pressure_app/model/export_import.dart'; +import 'package:file_saver/file_saver.dart' show MimeType; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -176,15 +177,6 @@ class Settings extends ChangeNotifier { notifyListeners(); } - bool get useExportCompatability { - return _prefs.getBool('useExportCompatability') ?? false; - } - - set useExportCompatability(bool useExportCompatability) { - _prefs.setBool('useExportCompatability', useExportCompatability); - notifyListeners(); - } - double get iconSize { return _prefs.getInt('iconSize')?.toDouble() ?? 30; } @@ -284,10 +276,66 @@ class Settings extends ChangeNotifier { ExportFormat get exportFormat { return ExportFormat(_prefs.getInt('exportFormat') ?? 0); } + set exportFormat(ExportFormat format) { _prefs.setInt('exportFormat', format.code); notifyListeners(); } + + String get csvFieldDelimiter { + return _prefs.getString('csvFieldDelimiter') ?? ','; + } + + set csvFieldDelimiter(String value) { + _prefs.setString('csvFieldDelimiter', value); + notifyListeners(); + } + + String get csvTextDelimiter { + return _prefs.getString('csvTextDelimiter') ?? '"'; + } + + set csvTextDelimiter(String value) { + _prefs.setString('csvTextDelimiter', value); + notifyListeners(); + } + + MimeType get exportMimeType { + switch (_prefs.getInt('exportMimeType') ?? 0) { + case 0: + return MimeType.csv; + case 1: + return MimeType.text; + case 2: + return MimeType.pdf; + case 3: + return MimeType.other; + default: + throw UnimplementedError(); + } + } + set exportMimeType(MimeType value) { + switch (value) { + case MimeType.csv: + _prefs.setInt('exportMimeType', 0); + break; + case MimeType.text: + _prefs.setInt('exportMimeType', 1); + break; + case MimeType.pdf: + _prefs.setInt('exportMimeType', 2); + break; + case MimeType.other: + _prefs.setInt('exportMimeType', 3); + break; + default: + throw UnimplementedError(); + } + notifyListeners(); + } + + + } class TimeStep { diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 4845ba2a..6b0daa76 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -2,6 +2,7 @@ import 'package:blood_pressure_app/components/settings_widgets.dart'; import 'package:blood_pressure_app/model/blood_pressure.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:blood_pressure_app/screens/subsettings/enter_timeformat.dart'; +import 'package:blood_pressure_app/screens/subsettings/export_import_screen.dart'; import 'package:blood_pressure_app/screens/subsettings/warn_about.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -243,15 +244,16 @@ class SettingsPage extends StatelessWidget { SettingsSection( title: Text(AppLocalizations.of(context)!.data), children: [ - SwitchSettingsTile( - key: const Key('useExportCompatability'), - initialValue: settings.useExportCompatability, - title: Text(AppLocalizations.of(context)!.useExportCompatability), - description: Text(AppLocalizations.of(context)!.useExportCompatabilityDesc), - leading: const Icon(Icons.support), - onToggle: (value) { - settings.useExportCompatability = value; - }), + SettingsTile( + title: const Text('EXPORT'), + trailing: const Icon(Icons.arrow_forward_ios), + onPressed: (context) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ExportImportScreen()), + ); + } + ), SettingsTile( key: const Key('export'), title: Text(AppLocalizations.of(context)!.export), @@ -264,7 +266,7 @@ class SettingsPage extends StatelessWidget { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(msg)))); } - }, exportAsText: settings.useExportCompatability), + }, exportAsText: false), ), SettingsTile( key: const Key('import'), diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 1f801318..3c49daec 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -2,6 +2,7 @@ import 'package:blood_pressure_app/components/settings_widgets.dart'; import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; +import 'package:file_saver/file_saver.dart' show MimeType; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -12,17 +13,56 @@ class ExportImportScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context)!.exportImport),), + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.exportImport), + backgroundColor: Theme.of(context).primaryColor, + ), body: Consumer(builder: (context, settings, child) { List modeSpecificSettings = []; if (settings.exportFormat == ExportFormat.csv) { - modeSpecificSettings = []; + modeSpecificSettings = [ + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.fieldDelimiter), + inputWidth: 40, + initialValue: settings.csvFieldDelimiter, + onEditingComplete: (value) { + if (value != null) { + settings.csvFieldDelimiter = value; + } + }, + ), + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.textDelimiter), + inputWidth: 40, + initialValue: settings.csvTextDelimiter, + onEditingComplete: (value) { + if (value != null) { + settings.csvTextDelimiter = value; + } + }, + ) + ]; } List options = [ + DropDownSettingsTile( + key: const Key('exportMimeType'), + title: Text(AppLocalizations.of(context)!.exportMimeType), + value: settings.exportMimeType, + items: [ + DropdownMenuItem(value: MimeType.csv, child: Text(AppLocalizations.of(context)!.csv)), + DropdownMenuItem(value: MimeType.text, child: Text(AppLocalizations.of(context)!.text)), + DropdownMenuItem(value: MimeType.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + DropdownMenuItem(value: MimeType.other, child: Text(AppLocalizations.of(context)!.other)), + ], + onChanged: (MimeType? value) { + if (value != null) { + settings.exportMimeType = value; + } + }, + ), DropDownSettingsTile( key: const Key('exportFormat'), - leading: const Icon(Icons.error), title: Text(AppLocalizations.of(context)!.exportFormat), value: settings.exportFormat, items: [ From 2db2fa9860ee4c88ac1ce6edc43ef8100a0a5163 Mon Sep 17 00:00:00 2001 From: derdilla Date: Sat, 17 Jun 2023 09:41:23 +0200 Subject: [PATCH 05/35] move export buttons to second screen --- lib/l10n/app_de.arb | 8 +- lib/l10n/app_en.arb | 7 +- lib/screens/settings.dart | 41 +-------- .../subsettings/export_import_screen.dart | 87 +++++++++++++++---- 4 files changed, 79 insertions(+), 64 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 71a9c34b..310ada6c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -82,11 +82,9 @@ "pdf": "PDF", "text": "Text", "other": "Anderes", - "fieldDelimiter": "Feld separator", + "fieldDelimiter": "Feldseparator", "textDelimiter": "Textbegrenzung", - "useExportCompatability": "Kompatibler Export", - "useExportCompatabilityDesc": "Signalisiert Export als Text", - "export": "Exportieren", + "export": "EXPORT", "exportSuccess": "Exportiert in: {path}", "@exportSuccess": { "placeholders": { @@ -96,7 +94,7 @@ } }, "shared": "Geteilt", - "import": "Import", + "import": "IMPORT", "sourceCode": "Quellcode", "licenses": "Lizenzen dritter", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6ac588ac..fb11fd41 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -78,15 +78,14 @@ "exportImport": "export / import", "exportFormat": "export format", "exportMimeType": "export MIME type", + "exportMimeTypeDesc": "signalizes type to other apps", "csv": "CSV", "pdf": "PDF", "text": "text", "other": "other", "fieldDelimiter": "field delimiter", "textDelimiter": "text delimiter", - "useExportCompatability": "compatability export", - "useExportCompatabilityDesc": "sets export mime type to text", - "export": "export", + "export": "EXPORT", "exportSuccess": "Exported to: {path}", "@exportSuccess": { "placeholders": { @@ -96,7 +95,7 @@ } }, "shared": "shared", - "import": "import", + "import": "IMPORT", "sourceCode": "source code", "licenses": "3rd party licenses", diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 6b0daa76..1b111798 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -1,5 +1,4 @@ import 'package:blood_pressure_app/components/settings_widgets.dart'; -import 'package:blood_pressure_app/model/blood_pressure.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:blood_pressure_app/screens/subsettings/enter_timeformat.dart'; import 'package:blood_pressure_app/screens/subsettings/export_import_screen.dart'; @@ -245,7 +244,8 @@ class SettingsPage extends StatelessWidget { title: Text(AppLocalizations.of(context)!.data), children: [ SettingsTile( - title: const Text('EXPORT'), + title: Text(AppLocalizations.of(context)!.exportImport), + leading: const Icon(Icons.download), trailing: const Icon(Icons.arrow_forward_ios), onPressed: (context) { Navigator.push( @@ -254,43 +254,6 @@ class SettingsPage extends StatelessWidget { ); } ), - SettingsTile( - key: const Key('export'), - title: Text(AppLocalizations.of(context)!.export), - leading: const Icon(Icons.save), - onPressed: (context) => Provider.of(context, listen: false).save((success, msg) { - if (success && msg != null) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.success(msg)))); - } else if (!success && msg != null) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(msg)))); - } - }, exportAsText: false), - ), - SettingsTile( - key: const Key('import'), - title: Text(AppLocalizations.of(context)!.import), - leading: const Icon(Icons.file_upload), - onPressed: (context) { - try { - Provider.of(context, listen: false).import((res) { - if (res) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: - Text(AppLocalizations.of(context)!.success(AppLocalizations.of(context)!.import)))); - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)! - .error(AppLocalizations.of(context)!.errNoFileOpened)))); - } - }); - } on Exception catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(e.toString())))); - } - }, - ), ], ), SettingsSection(title: const Text('about'), children: [ diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 3c49daec..0dda6bc5 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -1,8 +1,8 @@ import 'package:blood_pressure_app/components/settings_widgets.dart'; +import 'package:blood_pressure_app/model/blood_pressure.dart'; import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; -import 'package:file_saver/file_saver.dart' show MimeType; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -45,9 +45,25 @@ class ExportImportScreen extends StatelessWidget { } List options = [ + DropDownSettingsTile( + key: const Key('exportFormat'), + title: Text(AppLocalizations.of(context)!.exportFormat), + value: settings.exportFormat, + items: [ + DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), + DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + ], + onChanged: (ExportFormat? value) { + if (value != null) { + settings.exportFormat = value; + } + }, + ), + /* DropDownSettingsTile( key: const Key('exportMimeType'), title: Text(AppLocalizations.of(context)!.exportMimeType), + description: Text(AppLocalizations.of(context)!.exportMimeTypeDesc), value: settings.exportMimeType, items: [ DropdownMenuItem(value: MimeType.csv, child: Text(AppLocalizations.of(context)!.csv)), @@ -61,27 +77,66 @@ class ExportImportScreen extends StatelessWidget { } }, ), - DropDownSettingsTile( - key: const Key('exportFormat'), - title: Text(AppLocalizations.of(context)!.exportFormat), - value: settings.exportFormat, - items: [ - DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), - DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), - ], - onChanged: (ExportFormat? value) { - if (value != null) { - settings.exportFormat = value; - } - }, - ) + */ ]; options.addAll(modeSpecificSettings); - return ListView( children: options, ); }), + floatingActionButton: SizedBox( + height: 60, + child: Center( + child: Row( + children: [ + Expanded( + flex: 50, + child: MaterialButton( + height: 60, + child: Text(AppLocalizations.of(context)!.export), + onPressed: () { + Provider.of(context, listen: false).save((success, msg) { + if (success && msg != null) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.success(msg)))); + } else if (!success && msg != null) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(msg)))); + } + }, exportAsText: false); + }, + ) + ), + const VerticalDivider(), + Expanded( + flex: 50, + child: MaterialButton( + height: 60, + child: Text(AppLocalizations.of(context)!.import), + onPressed: () { + try { + Provider.of(context, listen: false).import((res) { + if (res) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: + Text(AppLocalizations.of(context)!.success(AppLocalizations.of(context)!.import)))); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)! + .error(AppLocalizations.of(context)!.errNoFileOpened)))); + } + }); + } on Exception catch (e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(e.toString())))); + } + }, + ) + ), + ], + ), + ), + ), ); } } From 52936a4f31ccea613ac1c8e89785e71a4a2254e2 Mon Sep 17 00:00:00 2001 From: derdilla Date: Sun, 18 Jun 2023 10:35:55 +0200 Subject: [PATCH 06/35] add export range --- lib/l10n/app_de.arb | 3 +++ lib/l10n/app_en.arb | 3 +++ lib/model/settings_store.dart | 10 ++++++++ .../subsettings/export_import_screen.dart | 23 +++++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 310ada6c..4cbaeb05 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -35,6 +35,7 @@ "errNoValue": "Bitte Wert eingeben", "errNotEnoughDataToGraph": "Zuwenig Daten für Graphen", "errNoData": "Keine Daten", + "errNoRangeForExport": "Sie müssen angeben, welche daten sie exportieren wollen.", "btnCancel": "ABBRUCH", "btnSave": "OK", @@ -75,7 +76,9 @@ "sysWarn": "Warnwert Sys", "diaWarn": "Warnwert Dia", "data": "Daten", + "exportImport": "Exportieren / Importieren", + "exportInterval": "Datenbereich", "exportFormat": "Exportformat", "exportMimeType": "Export MIME typ", "csv": "CSV", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fb11fd41..fbf1972f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -35,6 +35,7 @@ "errNoValue": "Please enter a value", "errNotEnoughDataToGraph": "not enough data to draw graph", "errNoData": "no data", + "errNoRangeForExport": "You need to specify a range in which data is exported.", "btnCancel": "CANCEL", "btnSave": "SAVE", @@ -75,7 +76,9 @@ "sysWarn": "systolic warn", "diaWarn": "diastolic warn", "data": "data", + "exportImport": "export / import", + "exportInterval": "data range", "exportFormat": "export format", "exportMimeType": "export MIME type", "exportMimeTypeDesc": "signalizes type to other apps", diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 2adfdf2d..81c39850 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -11,6 +11,8 @@ class Settings extends ChangeNotifier { DateTime? _displayDataStart; DateTime? _displayDataEnd; + DateTimeRange? _exportDataRange; + Settings._create(); // factory method, to allow for async constructor static Future create() async { @@ -334,7 +336,15 @@ class Settings extends ChangeNotifier { notifyListeners(); } + DateTimeRange? get exportDataRange { + return _exportDataRange; + } + set exportDataRange(DateTimeRange? value) { + _exportDataRange = value; + notifyListeners(); + } + } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 0dda6bc5..d08e5db6 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -5,6 +5,7 @@ import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class ExportImportScreen extends StatelessWidget { @@ -18,6 +19,13 @@ class ExportImportScreen extends StatelessWidget { backgroundColor: Theme.of(context).primaryColor, ), body: Consumer(builder: (context, settings, child) { + var exportRange = settings.exportDataRange; + String? exportRangeText; + if (exportRange != null) { + var formatter = DateFormat.yMMMd(AppLocalizations.of(context)!.localeName); + exportRangeText = '${formatter.format(exportRange.start)} - ${formatter.format(exportRange.end)}'; + } + List modeSpecificSettings = []; if (settings.exportFormat == ExportFormat.csv) { modeSpecificSettings = [ @@ -45,6 +53,21 @@ class ExportImportScreen extends StatelessWidget { } List options = [ + SettingsTile( + title: Text(AppLocalizations.of(context)!.exportInterval), + description: (exportRangeText != null) ? Text(exportRangeText) : null, + onPressed: (context) async { + var model = Provider.of(context, listen: false); + var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); + if (newRange == null && context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); + return; + } + settings.exportDataRange = newRange; + + } + ), DropDownSettingsTile( key: const Key('exportFormat'), title: Text(AppLocalizations.of(context)!.exportFormat), From 8ef819eaabf16e78927f52e47436199cbf63eac0 Mon Sep 17 00:00:00 2001 From: derdilla Date: Sun, 18 Jun 2023 17:29:02 +0200 Subject: [PATCH 07/35] add entry order option --- .../subsettings/export_import_screen.dart | 123 +++++++++++++++++- 1 file changed, 120 insertions(+), 3 deletions(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index d08e5db6..80a4087a 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -48,7 +48,8 @@ class ExportImportScreen extends StatelessWidget { settings.csvTextDelimiter = value; } }, - ) + ), + CsvItemsOrderCreator() ]; } @@ -117,8 +118,20 @@ class ExportImportScreen extends StatelessWidget { child: MaterialButton( height: 60, child: Text(AppLocalizations.of(context)!.export), - onPressed: () { - Provider.of(context, listen: false).save((success, msg) { + onPressed: () async { + var settings = Provider.of(context); + var range = settings.exportDataRange; + if (range == null) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); + return; + } + + var entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange!.start, settings.exportDataRange!.end); + var fileContents = DataExporter(settings).createFile(entries); + + /* + .save((success, msg) { if (success && msg != null) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.success(msg)))); @@ -127,6 +140,7 @@ class ExportImportScreen extends StatelessWidget { .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(msg)))); } }, exportAsText: false); + */ }, ) ), @@ -163,3 +177,106 @@ class ExportImportScreen extends StatelessWidget { ); } } + +class CsvItemsOrderCreator extends StatefulWidget { + @override + State createState() => _CsvItemsOrderCreatorState(); +} + +class _CsvItemsOrderCreatorState extends State { + List _items = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; + List _addable = []; + + @override + Widget build(BuildContext context) { + print(_addable); + return Container( + margin: const EdgeInsets.fromLTRB(45, 20, 10, 0), + padding: const EdgeInsets.all(20), + height: 320, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).textTheme.labelLarge?.color ?? Colors.teal), + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + clipBehavior: Clip.hardEdge, + child: ReorderableListView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + onReorder: (oldIndex, newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final String item = _items.removeAt(oldIndex); + _items.insert(newIndex, item); + }); + }, + footer: (_addable.isNotEmpty) ? InkWell( + onTap: () { + showDialog(context: context, + builder: (context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(50)) + ), + child: Container( + height: 330, + padding: const EdgeInsets.all(30), + child: ListView( + children: [ + for (int i = 0; i < _addable.length; i += 1) + ListTile( + title: Text(_addable[i]), + onTap: () { + setState(() { + var addedItem = _addable.removeAt(i); + _items.add(addedItem); + }); + Navigator.of(context).pop(); + + }, + ) + ], + ), + ), + ); + } + ); + }, + child: const Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add), + SizedBox(width: 10,), + Text('ADD ENTRY') + ], + ), + ), + ) : null, + children: [ + for (int i = 0; i < _items.length; i += 1) + SizedBox( + key: Key(_items[i]), + child: Dismissible( + key: Key('dism$_items[i]'), + background: Container(color: Colors.red), + onDismissed: (direction) { + setState(() { + var removedItem = _items.removeAt(i); + _addable.add(removedItem); + }); + }, + child: ListTile( + title: Text(_items[i]), + trailing: const Icon(Icons.drag_handle), + ), + ), + ), + ], + ), + ); + } + +} + From f5d1b3262a934b10ea2da23fbdb602c798866153 Mon Sep 17 00:00:00 2001 From: derdilla Date: Sun, 18 Jun 2023 19:05:41 +0200 Subject: [PATCH 08/35] save export entry order in different scope --- lib/model/settings_store.dart | 21 ++- .../subsettings/export_import_screen.dart | 163 ++++++++---------- 2 files changed, 96 insertions(+), 88 deletions(-) diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 81c39850..049815ae 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -12,7 +12,9 @@ class Settings extends ChangeNotifier { DateTime? _displayDataEnd; DateTimeRange? _exportDataRange; - + List _exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; + List _exportAddableItems = []; + Settings._create(); // factory method, to allow for async constructor static Future create() async { @@ -345,6 +347,23 @@ class Settings extends ChangeNotifier { notifyListeners(); } + List get exportAddableItems { + return _exportAddableItems; + } + + set exportAddableItems(List value) { + _exportAddableItems = value; + notifyListeners(); + } + List get exportItems { + return _exportItems; + } + + set exportItems(List value) { + _exportItems = value; + notifyListeners(); + } + } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 80a4087a..3e5975c4 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -178,104 +178,93 @@ class ExportImportScreen extends StatelessWidget { } } -class CsvItemsOrderCreator extends StatefulWidget { - @override - State createState() => _CsvItemsOrderCreatorState(); -} - -class _CsvItemsOrderCreatorState extends State { - List _items = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; - List _addable = []; +class CsvItemsOrderCreator extends StatelessWidget { + const CsvItemsOrderCreator({super.key}); @override Widget build(BuildContext context) { - print(_addable); - return Container( - margin: const EdgeInsets.fromLTRB(45, 20, 10, 0), - padding: const EdgeInsets.all(20), - height: 320, - decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).textTheme.labelLarge?.color ?? Colors.teal), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - clipBehavior: Clip.hardEdge, - child: ReorderableListView( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - onReorder: (oldIndex, newIndex) { - setState(() { + return Consumer(builder: (context, settings, child) { + return Container( + margin: const EdgeInsets.fromLTRB(45, 20, 10, 0), + padding: const EdgeInsets.all(20), + height: 320, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).textTheme.labelLarge?.color ?? Colors.teal), + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + clipBehavior: Clip.hardEdge, + child: ReorderableListView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + onReorder: (oldIndex, newIndex) { if (oldIndex < newIndex) { newIndex -= 1; } - final String item = _items.removeAt(oldIndex); - _items.insert(newIndex, item); - }); - }, - footer: (_addable.isNotEmpty) ? InkWell( - onTap: () { - showDialog(context: context, - builder: (context) { - return Dialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(50)) - ), - child: Container( - height: 330, - padding: const EdgeInsets.all(30), - child: ListView( - children: [ - for (int i = 0; i < _addable.length; i += 1) - ListTile( - title: Text(_addable[i]), - onTap: () { - setState(() { - var addedItem = _addable.removeAt(i); - _items.add(addedItem); - }); - Navigator.of(context).pop(); + final String item = settings.exportItems.removeAt(oldIndex); + settings.exportItems.insert(newIndex, item); + }, + footer: (settings.exportAddableItems.isNotEmpty) ? InkWell( + onTap: () { + showDialog(context: context, + builder: (context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(50)) + ), + child: Container( + height: 330, + padding: const EdgeInsets.all(30), + child: ListView( + children: [ + for (int i = 0; i < settings.exportAddableItems.length; i += 1) + ListTile( + title: Text(settings.exportAddableItems[i]), + onTap: () { + var addedItem = settings.exportAddableItems.removeAt(i); + settings.exportItems.add(addedItem); + Navigator.of(context).pop(); - }, - ) - ], + }, + ) + ], + ), ), - ), - ); - } - ); - }, - child: const Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.add), - SizedBox(width: 10,), - Text('ADD ENTRY') - ], + ); + } + ); + }, + child: const Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add), + SizedBox(width: 10,), + Text('ADD ENTRY') + ], + ), ), - ), - ) : null, - children: [ - for (int i = 0; i < _items.length; i += 1) - SizedBox( - key: Key(_items[i]), - child: Dismissible( - key: Key('dism$_items[i]'), - background: Container(color: Colors.red), - onDismissed: (direction) { - setState(() { - var removedItem = _items.removeAt(i); - _addable.add(removedItem); - }); - }, - child: ListTile( - title: Text(_items[i]), - trailing: const Icon(Icons.drag_handle), + ) : null, + children: [ + for (int i = 0; i < settings.exportItems.length; i += 1) + SizedBox( + key: Key(settings.exportItems[i]), + child: Dismissible( + key: Key('dism${settings.exportItems[i]}'), + background: Container(color: Colors.red), + onDismissed: (direction) { + var removedItem = settings.exportItems.removeAt(i); + settings.exportAddableItems.add(removedItem); + }, + child: ListTile( + title: Text(settings.exportItems[i]), + trailing: const Icon(Icons.drag_handle), + ), ), ), - ), - ], - ), - ); + ], + ), + ); + }); } } From 1e8593db9e5411d33c173ac367447e0e488a3e70 Mon Sep 17 00:00:00 2001 From: derdilla Date: Mon, 19 Jun 2023 18:23:01 +0200 Subject: [PATCH 09/35] make export range conditional --- lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/model/export_import.dart | 56 +++++++- lib/model/settings_store.dart | 13 ++ .../subsettings/export_import_screen.dart | 122 ++++++++++-------- 5 files changed, 136 insertions(+), 57 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 4cbaeb05..e7849864 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -78,6 +78,7 @@ "data": "Daten", "exportImport": "Exportieren / Importieren", + "exportLimitDataRange": "Datenbereich einschränken", "exportInterval": "Datenbereich", "exportFormat": "Exportformat", "exportMimeType": "Export MIME typ", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fbf1972f..caa185fd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -78,6 +78,7 @@ "data": "data", "exportImport": "export / import", + "exportLimitDataRange": "limit data range", "exportInterval": "data range", "exportFormat": "export format", "exportMimeType": "export MIME type", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 8aaa6f25..6ed0ea96 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -1,12 +1,56 @@ -import 'package:intl/intl.dart'; +import 'dart:convert'; +import 'dart:typed_data'; -class CSVExportSettings { - final DateFormat dateFormatter; - final String? fieldDelimiter; - final String? textDelimiter; +import 'package:blood_pressure_app/model/settings_store.dart'; +import 'package:csv/csv.dart'; - CSVExportSettings(this.dateFormatter, this.fieldDelimiter, this.textDelimiter); +import 'blood_pressure.dart'; + +class DataExporter { + Settings settings; + + DataExporter(this.settings); + + Uint8List createFile(List records) { + if (settings.exportFormat == ExportFormat.csv) { + var csvHead = ''; + for (var attribute in settings.exportItems) { + csvHead += attribute; + csvHead += settings.csvFieldDelimiter; + } + csvHead += '\n'; + + List> items = []; + for (var record in records) { + List row = []; + for (var attribute in settings.exportItems) { + switch (attribute) { + case 'timestampUnixMs': + row.add(record.creationTime.millisecondsSinceEpoch); + break; + case 'systolic': + row.add(record.systolic); + break; + case 'diastolic': + row.add(record.diastolic); + break; + case 'pulse': + row.add(record.pulse); + break; + case 'notes': + row.add(record.notes); + break; + } + } + items.add(row); + } + var converter = ListToCsvConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter); + var csvData = converter.convert(items); + return Uint8List.fromList(utf8.encode(csvHead + csvData)); + } + return Uint8List(0); + } } class ExportFormat { diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 049815ae..381874e6 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -23,6 +23,10 @@ class Settings extends ChangeNotifier { return component; } + void forceNotifyListeners() { + notifyListeners(); + } + int get graphStepSize { return _prefs.getInt('graphStepSize') ?? TimeStep.day; } @@ -338,6 +342,15 @@ class Settings extends ChangeNotifier { notifyListeners(); } + bool get exportLimitDataRange { + return _prefs.getBool('exportLimitDataRange') ?? false; + } + + set exportLimitDataRange(bool value) { + _prefs.setBool('exportLimitDataRange', value); + notifyListeners(); + } + DateTimeRange? get exportDataRange { return _exportDataRange; } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 3e5975c4..02ae4df5 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -1,12 +1,16 @@ +import 'dart:io'; + import 'package:blood_pressure_app/components/settings_widgets.dart'; import 'package:blood_pressure_app/model/blood_pressure.dart'; import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; class ExportImportScreen extends StatelessWidget { const ExportImportScreen({super.key}); @@ -54,21 +58,28 @@ class ExportImportScreen extends StatelessWidget { } List options = [ - SettingsTile( - title: Text(AppLocalizations.of(context)!.exportInterval), - description: (exportRangeText != null) ? Text(exportRangeText) : null, - onPressed: (context) async { - var model = Provider.of(context, listen: false); - var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); - if (newRange == null && context.mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); - return; + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportLimitDataRange), + initialValue: settings.exportLimitDataRange, + onToggle: (value) { + settings.exportLimitDataRange = value; } - settings.exportDataRange = newRange; - - } ), + (settings.exportLimitDataRange) ? SettingsTile( + title: Text(AppLocalizations.of(context)!.exportInterval), + description: (exportRangeText != null) ? Text(exportRangeText) : null, + onPressed: (context) async { + var model = Provider.of(context, listen: false); + var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); + if (newRange == null && context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); + return; + } + settings.exportDataRange = newRange; + + } + ) : const SizedBox.shrink(), DropDownSettingsTile( key: const Key('exportFormat'), title: Text(AppLocalizations.of(context)!.exportFormat), @@ -119,7 +130,7 @@ class ExportImportScreen extends StatelessWidget { height: 60, child: Text(AppLocalizations.of(context)!.export), onPressed: () async { - var settings = Provider.of(context); + var settings = Provider.of(context, listen: false); var range = settings.exportDataRange; if (range == null) { ScaffoldMessenger.of(context) @@ -130,17 +141,23 @@ class ExportImportScreen extends StatelessWidget { var entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange!.start, settings.exportDataRange!.end); var fileContents = DataExporter(settings).createFile(entries); - /* - .save((success, msg) { - if (success && msg != null) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.success(msg)))); - } else if (!success && msg != null) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(msg)))); - } - }, exportAsText: false); - */ + String filename = 'blood_press_${DateTime.now().toIso8601String()}'; + String path = await FileSaver.instance.saveFile(name: filename, bytes: fileContents); + + if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) && context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.success(path)))); + } else if (Platform.isAndroid || Platform.isIOS) { + Share.shareXFiles([ + XFile( + path, + mimeType: MimeType.csv.type + ) + ]); + } else { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('UNSUPPORTED PLATFORM'))); + } }, ) ), @@ -202,38 +219,40 @@ class CsvItemsOrderCreator extends StatelessWidget { } final String item = settings.exportItems.removeAt(oldIndex); settings.exportItems.insert(newIndex, item); + settings.forceNotifyListeners(); }, footer: (settings.exportAddableItems.isNotEmpty) ? InkWell( - onTap: () { - showDialog(context: context, - builder: (context) { - return Dialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(50)) - ), - child: Container( - height: 330, - padding: const EdgeInsets.all(30), - child: ListView( - children: [ - for (int i = 0; i < settings.exportAddableItems.length; i += 1) - ListTile( - title: Text(settings.exportAddableItems[i]), - onTap: () { - var addedItem = settings.exportAddableItems.removeAt(i); - settings.exportItems.add(addedItem); - Navigator.of(context).pop(); + onTap: () async { + await showDialog(context: context, + builder: (context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(50)) + ), + child: Container( + height: 330, + padding: const EdgeInsets.all(30), + child: ListView( + children: [ + for (int i = 0; i < settings.exportAddableItems.length; i += 1) + ListTile( + title: Text(settings.exportAddableItems[i]), + onTap: () { + var addedItem = settings.exportAddableItems.removeAt(i); + settings.exportItems.add(addedItem); + Navigator.of(context).pop(); - }, - ) - ], - ), + }, + ) + ], ), - ); - } + ), + ); + } ); + settings.forceNotifyListeners(); }, - child: const Center( + child: const Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -254,6 +273,7 @@ class CsvItemsOrderCreator extends StatelessWidget { onDismissed: (direction) { var removedItem = settings.exportItems.removeAt(i); settings.exportAddableItems.add(removedItem); + settings.forceNotifyListeners(); }, child: ListTile( title: Text(settings.exportItems[i]), From a038eb84ecfffb79b7ea6ae36b8d0effc801b8e2 Mon Sep 17 00:00:00 2001 From: derdilla Date: Mon, 19 Jun 2023 18:34:29 +0200 Subject: [PATCH 10/35] make export settings persistent --- lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/model/settings_store.dart | 23 +++++++++---------- .../subsettings/export_import_screen.dart | 16 +++++++------ 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e7849864..758efe3b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -36,6 +36,7 @@ "errNotEnoughDataToGraph": "Zuwenig Daten für Graphen", "errNoData": "Keine Daten", "errNoRangeForExport": "Sie müssen angeben, welche daten sie exportieren wollen.", + "errPleaseSelect": "Bitte auswählen", "btnCancel": "ABBRUCH", "btnSave": "OK", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index caa185fd..ed376a50 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -36,6 +36,7 @@ "errNotEnoughDataToGraph": "not enough data to draw graph", "errNoData": "no data", "errNoRangeForExport": "You need to specify a range in which data is exported.", + "errPleaseSelect": "please select", "btnCancel": "CANCEL", "btnSave": "SAVE", diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 381874e6..1c89522c 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -10,10 +10,6 @@ class Settings extends ChangeNotifier { DateTime? _displayDataStart; DateTime? _displayDataEnd; - - DateTimeRange? _exportDataRange; - List _exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; - List _exportAddableItems = []; Settings._create(); // factory method, to allow for async constructor @@ -351,29 +347,32 @@ class Settings extends ChangeNotifier { notifyListeners(); } - DateTimeRange? get exportDataRange { - return _exportDataRange; + DateTimeRange get exportDataRange { + final start = DateTime.fromMillisecondsSinceEpoch(_prefs.getInt('exportDataRangeStartEpochMs') ?? 0); + final end = DateTime.fromMillisecondsSinceEpoch(_prefs.getInt('exportDataRangeEndEpochMs') ?? 0); + return DateTimeRange(start: start, end: end); } - set exportDataRange(DateTimeRange? value) { - _exportDataRange = value; + set exportDataRange(DateTimeRange value) { + _prefs.setInt('exportDataRangeStartEpochMs', value.start.millisecondsSinceEpoch); + _prefs.setInt('exportDataRangeEndEpochMs', value.end.millisecondsSinceEpoch); notifyListeners(); } List get exportAddableItems { - return _exportAddableItems; + return _prefs.getStringList('exportAddableItems') ?? []; } set exportAddableItems(List value) { - _exportAddableItems = value; + _prefs.setStringList('exportAddableItems', value); notifyListeners(); } List get exportItems { - return _exportItems; + return _prefs.getStringList('exportItems') ?? ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; } set exportItems(List value) { - _exportItems = value; + _prefs.setStringList('exportItems', value); notifyListeners(); } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 02ae4df5..e86c0c14 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -24,10 +24,12 @@ class ExportImportScreen extends StatelessWidget { ), body: Consumer(builder: (context, settings, child) { var exportRange = settings.exportDataRange; - String? exportRangeText; - if (exportRange != null) { + String exportRangeText; + if (exportRange.start.millisecondsSinceEpoch != 0 && exportRange.end.millisecondsSinceEpoch != 0) { var formatter = DateFormat.yMMMd(AppLocalizations.of(context)!.localeName); exportRangeText = '${formatter.format(exportRange.start)} - ${formatter.format(exportRange.end)}'; + } else { + exportRangeText = AppLocalizations.of(context)!.errPleaseSelect; } List modeSpecificSettings = []; @@ -53,7 +55,7 @@ class ExportImportScreen extends StatelessWidget { } }, ), - CsvItemsOrderCreator() + const CsvItemsOrderCreator() ]; } @@ -67,7 +69,7 @@ class ExportImportScreen extends StatelessWidget { ), (settings.exportLimitDataRange) ? SettingsTile( title: Text(AppLocalizations.of(context)!.exportInterval), - description: (exportRangeText != null) ? Text(exportRangeText) : null, + description: Text(exportRangeText), onPressed: (context) async { var model = Provider.of(context, listen: false); var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); @@ -76,7 +78,7 @@ class ExportImportScreen extends StatelessWidget { .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); return; } - settings.exportDataRange = newRange; + settings.exportDataRange = newRange ?? DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(0), end: DateTime.fromMillisecondsSinceEpoch(0)); } ) : const SizedBox.shrink(), @@ -132,13 +134,13 @@ class ExportImportScreen extends StatelessWidget { onPressed: () async { var settings = Provider.of(context, listen: false); var range = settings.exportDataRange; - if (range == null) { + if (range.start.millisecondsSinceEpoch == 0 || range.end.millisecondsSinceEpoch == 0) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); return; } - var entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange!.start, settings.exportDataRange!.end); + var entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange.start, settings.exportDataRange.end); var fileContents = DataExporter(settings).createFile(entries); String filename = 'blood_press_${DateTime.now().toIso8601String()}'; From aed54eb8a22b5fa3a2b64a5fc2a088a7411b5d12 Mon Sep 17 00:00:00 2001 From: derdilla Date: Mon, 19 Jun 2023 18:43:58 +0200 Subject: [PATCH 11/35] fix export screen display on small devices --- .../subsettings/export_import_screen.dart | 182 +++++++++--------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index e86c0c14..242dc22e 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -22,105 +22,109 @@ class ExportImportScreen extends StatelessWidget { title: Text(AppLocalizations.of(context)!.exportImport), backgroundColor: Theme.of(context).primaryColor, ), - body: Consumer(builder: (context, settings, child) { - var exportRange = settings.exportDataRange; - String exportRangeText; - if (exportRange.start.millisecondsSinceEpoch != 0 && exportRange.end.millisecondsSinceEpoch != 0) { - var formatter = DateFormat.yMMMd(AppLocalizations.of(context)!.localeName); - exportRangeText = '${formatter.format(exportRange.start)} - ${formatter.format(exportRange.end)}'; - } else { - exportRangeText = AppLocalizations.of(context)!.errPleaseSelect; - } + body: Container( + margin: const EdgeInsets.only(bottom: 80), + child: Consumer(builder: (context, settings, child) { + var exportRange = settings.exportDataRange; + String exportRangeText; + if (exportRange.start.millisecondsSinceEpoch != 0 && exportRange.end.millisecondsSinceEpoch != 0) { + var formatter = DateFormat.yMMMd(AppLocalizations.of(context)!.localeName); + exportRangeText = '${formatter.format(exportRange.start)} - ${formatter.format(exportRange.end)}'; + } else { + exportRangeText = AppLocalizations.of(context)!.errPleaseSelect; + } - List modeSpecificSettings = []; - if (settings.exportFormat == ExportFormat.csv) { - modeSpecificSettings = [ - InputSettingsTile( - title: Text(AppLocalizations.of(context)!.fieldDelimiter), - inputWidth: 40, - initialValue: settings.csvFieldDelimiter, - onEditingComplete: (value) { + List modeSpecificSettings = []; + if (settings.exportFormat == ExportFormat.csv) { + modeSpecificSettings = [ + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.fieldDelimiter), + inputWidth: 40, + initialValue: settings.csvFieldDelimiter, + onEditingComplete: (value) { + if (value != null) { + settings.csvFieldDelimiter = value; + } + }, + ), + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.textDelimiter), + inputWidth: 40, + initialValue: settings.csvTextDelimiter, + onEditingComplete: (value) { + if (value != null) { + settings.csvTextDelimiter = value; + } + }, + ), + const CsvItemsOrderCreator() + ]; + } + + List options = [ + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportLimitDataRange), + initialValue: settings.exportLimitDataRange, + onToggle: (value) { + settings.exportLimitDataRange = value; + } + ), + (settings.exportLimitDataRange) ? SettingsTile( + title: Text(AppLocalizations.of(context)!.exportInterval), + description: Text(exportRangeText), + onPressed: (context) async { + var model = Provider.of(context, listen: false); + var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); + if (newRange == null && context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); + return; + } + settings.exportDataRange = newRange ?? DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(0), end: DateTime.fromMillisecondsSinceEpoch(0)); + + } + ) : const SizedBox.shrink(), + DropDownSettingsTile( + key: const Key('exportFormat'), + title: Text(AppLocalizations.of(context)!.exportFormat), + value: settings.exportFormat, + items: [ + DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), + DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + ], + onChanged: (ExportFormat? value) { if (value != null) { - settings.csvFieldDelimiter = value; + settings.exportFormat = value; } }, ), - InputSettingsTile( - title: Text(AppLocalizations.of(context)!.textDelimiter), - inputWidth: 40, - initialValue: settings.csvTextDelimiter, - onEditingComplete: (value) { + /* + DropDownSettingsTile( + key: const Key('exportMimeType'), + title: Text(AppLocalizations.of(context)!.exportMimeType), + description: Text(AppLocalizations.of(context)!.exportMimeTypeDesc), + value: settings.exportMimeType, + items: [ + DropdownMenuItem(value: MimeType.csv, child: Text(AppLocalizations.of(context)!.csv)), + DropdownMenuItem(value: MimeType.text, child: Text(AppLocalizations.of(context)!.text)), + DropdownMenuItem(value: MimeType.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + DropdownMenuItem(value: MimeType.other, child: Text(AppLocalizations.of(context)!.other)), + ], + onChanged: (MimeType? value) { if (value != null) { - settings.csvTextDelimiter = value; + settings.exportMimeType = value; } }, ), - const CsvItemsOrderCreator() + */ ]; - } - - List options = [ - SwitchSettingsTile( - title: Text(AppLocalizations.of(context)!.exportLimitDataRange), - initialValue: settings.exportLimitDataRange, - onToggle: (value) { - settings.exportLimitDataRange = value; - } - ), - (settings.exportLimitDataRange) ? SettingsTile( - title: Text(AppLocalizations.of(context)!.exportInterval), - description: Text(exportRangeText), - onPressed: (context) async { - var model = Provider.of(context, listen: false); - var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); - if (newRange == null && context.mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); - return; - } - settings.exportDataRange = newRange ?? DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(0), end: DateTime.fromMillisecondsSinceEpoch(0)); - - } - ) : const SizedBox.shrink(), - DropDownSettingsTile( - key: const Key('exportFormat'), - title: Text(AppLocalizations.of(context)!.exportFormat), - value: settings.exportFormat, - items: [ - DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), - DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), - ], - onChanged: (ExportFormat? value) { - if (value != null) { - settings.exportFormat = value; - } - }, - ), - /* - DropDownSettingsTile( - key: const Key('exportMimeType'), - title: Text(AppLocalizations.of(context)!.exportMimeType), - description: Text(AppLocalizations.of(context)!.exportMimeTypeDesc), - value: settings.exportMimeType, - items: [ - DropdownMenuItem(value: MimeType.csv, child: Text(AppLocalizations.of(context)!.csv)), - DropdownMenuItem(value: MimeType.text, child: Text(AppLocalizations.of(context)!.text)), - DropdownMenuItem(value: MimeType.pdf, child: Text(AppLocalizations.of(context)!.pdf)), - DropdownMenuItem(value: MimeType.other, child: Text(AppLocalizations.of(context)!.other)), - ], - onChanged: (MimeType? value) { - if (value != null) { - settings.exportMimeType = value; - } - }, - ), - */ - ]; - options.addAll(modeSpecificSettings); - return ListView( - children: options, - ); - }), + options.addAll(modeSpecificSettings); + options.add(const SizedBox(height: 20,)); + return ListView( + children: options, + ); + }), + ), floatingActionButton: SizedBox( height: 60, child: Center( From 5b68973ea518dfb476433689a216c87d16303916 Mon Sep 17 00:00:00 2001 From: derdilla Date: Tue, 20 Jun 2023 08:49:52 +0200 Subject: [PATCH 12/35] add all entries to default export range --- .../subsettings/export_import_screen.dart | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 242dc22e..fcf7632e 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -1,4 +1,5 @@ +import 'dart:collection'; import 'dart:io'; import 'package:blood_pressure_app/components/settings_widgets.dart'; @@ -137,14 +138,19 @@ class ExportImportScreen extends StatelessWidget { child: Text(AppLocalizations.of(context)!.export), onPressed: () async { var settings = Provider.of(context, listen: false); - var range = settings.exportDataRange; - if (range.start.millisecondsSinceEpoch == 0 || range.end.millisecondsSinceEpoch == 0) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); - return; - } - var entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange.start, settings.exportDataRange.end); + final UnmodifiableListView entries; + if (settings.exportLimitDataRange) { + var range = settings.exportDataRange; + if (range.start.millisecondsSinceEpoch == 0 || range.end.millisecondsSinceEpoch == 0) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); + return; + } + entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange.start, settings.exportDataRange.end); + } else { + entries = await Provider.of(context, listen: false).all; + } var fileContents = DataExporter(settings).createFile(entries); String filename = 'blood_press_${DateTime.now().toIso8601String()}'; From 5b9f69efc6b421e6626c83e3fac2d862ac9d16ee Mon Sep 17 00:00:00 2001 From: derdilla Date: Tue, 20 Jun 2023 09:04:35 +0200 Subject: [PATCH 13/35] fix export entry order with persistent storage --- lib/model/export_import.dart | 2 ++ .../subsettings/export_import_screen.dart | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 6ed0ea96..85d120ae 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -48,6 +48,8 @@ class DataExporter { var converter = ListToCsvConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter); var csvData = converter.convert(items); return Uint8List.fromList(utf8.encode(csvHead + csvData)); + } else if (settings.exportFormat == ExportFormat.pdf) { + throw UnimplementedError('TODO'); } return Uint8List(0); } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index fcf7632e..cae8ff1e 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -229,14 +229,18 @@ class CsvItemsOrderCreator extends StatelessWidget { if (oldIndex < newIndex) { newIndex -= 1; } - final String item = settings.exportItems.removeAt(oldIndex); - settings.exportItems.insert(newIndex, item); - settings.forceNotifyListeners(); + var exportItems = settings.exportItems; + final String item = exportItems.removeAt(oldIndex); + exportItems.insert(newIndex, item); + + settings.exportItems = exportItems; }, footer: (settings.exportAddableItems.isNotEmpty) ? InkWell( onTap: () async { await showDialog(context: context, builder: (context) { + var exportItems = settings.exportItems; + var exportAddableItems = settings.exportAddableItems; return Dialog( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(50)) @@ -246,14 +250,15 @@ class CsvItemsOrderCreator extends StatelessWidget { padding: const EdgeInsets.all(30), child: ListView( children: [ - for (int i = 0; i < settings.exportAddableItems.length; i += 1) + for (int i = 0; i < exportAddableItems.length; i += 1) ListTile( - title: Text(settings.exportAddableItems[i]), + title: Text(exportAddableItems[i]), onTap: () { - var addedItem = settings.exportAddableItems.removeAt(i); - settings.exportItems.add(addedItem); + var addedItem = exportAddableItems.removeAt(i); + exportItems.add(addedItem); Navigator.of(context).pop(); - + settings.exportItems = exportItems; + settings.exportAddableItems = exportAddableItems; }, ) ], @@ -262,7 +267,6 @@ class CsvItemsOrderCreator extends StatelessWidget { ); } ); - settings.forceNotifyListeners(); }, child: const Center( child: Row( @@ -283,9 +287,12 @@ class CsvItemsOrderCreator extends StatelessWidget { key: Key('dism${settings.exportItems[i]}'), background: Container(color: Colors.red), onDismissed: (direction) { - var removedItem = settings.exportItems.removeAt(i); - settings.exportAddableItems.add(removedItem); - settings.forceNotifyListeners(); + var exportItems = settings.exportItems; + var exportAddableItems = settings.exportAddableItems; + var removedItem = exportItems.removeAt(i); + exportAddableItems.add(removedItem); + settings.exportItems = exportItems; + settings.exportAddableItems = exportAddableItems; }, child: ListTile( title: Text(settings.exportItems[i]), From 35983a1bb0457a1285427a6a1f54d0161572a1a2 Mon Sep 17 00:00:00 2001 From: derdilla Date: Tue, 20 Jun 2023 09:20:12 +0200 Subject: [PATCH 14/35] make custom export entries optional --- lib/l10n/app_de.arb | 2 ++ lib/l10n/app_en.arb | 2 ++ lib/model/export_import.dart | 17 ++++++++--- lib/model/settings_store.dart | 9 ++++++ .../subsettings/export_import_screen.dart | 28 +++++++++++++------ 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 758efe3b..8b9bdd29 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -82,6 +82,8 @@ "exportLimitDataRange": "Datenbereich einschränken", "exportInterval": "Datenbereich", "exportFormat": "Exportformat", + "exportCustomEntries": "Eigene Felder", + "addEntry": "Feld hinzufügen", "exportMimeType": "Export MIME typ", "csv": "CSV", "pdf": "PDF", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ed376a50..53687117 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -82,6 +82,8 @@ "exportLimitDataRange": "limit data range", "exportInterval": "data range", "exportFormat": "export format", + "exportCustomEntries": "customize fields", + "addEntry": "Feld hinzufügen", "exportMimeType": "export MIME type", "exportMimeTypeDesc": "signalizes type to other apps", "csv": "CSV", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 85d120ae..4d135899 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -14,17 +14,26 @@ class DataExporter { Uint8List createFile(List records) { if (settings.exportFormat == ExportFormat.csv) { + List exportItems; + if (settings.exportCustomEntries) { + exportItems = settings.exportItems; + } else { + exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; + } + var csvHead = ''; - for (var attribute in settings.exportItems) { - csvHead += attribute; - csvHead += settings.csvFieldDelimiter; + for (var i = 0; i> items = []; for (var record in records) { List row = []; - for (var attribute in settings.exportItems) { + for (var attribute in exportItems) { switch (attribute) { case 'timestampUnixMs': row.add(record.creationTime.millisecondsSinceEpoch); diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 1c89522c..458410e0 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -358,6 +358,15 @@ class Settings extends ChangeNotifier { _prefs.setInt('exportDataRangeEndEpochMs', value.end.millisecondsSinceEpoch); notifyListeners(); } + + bool get exportCustomEntries { + return _prefs.getBool('exportCustomEntries') ?? false; + } + + set exportCustomEntries(bool value) { + _prefs.setBool('exportCustomEntries', value); + notifyListeners(); + } List get exportAddableItems { return _prefs.getStringList('exportAddableItems') ?? []; diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index cae8ff1e..d2193359 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -58,7 +58,14 @@ class ExportImportScreen extends StatelessWidget { } }, ), - const CsvItemsOrderCreator() + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportCustomEntries), + initialValue: settings.exportCustomEntries, + onToggle: (value) { + settings.exportCustomEntries = value; + } + ), + (settings.exportCustomEntries) ? const CsvItemsOrderCreator(): const SizedBox.shrink() ]; } @@ -268,14 +275,17 @@ class CsvItemsOrderCreator extends StatelessWidget { } ); }, - child: const Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.add), - SizedBox(width: 10,), - Text('ADD ENTRY') - ], + child: Container( + margin: const EdgeInsets.only(top: 15), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.add), + const SizedBox(width: 10,), + Text(AppLocalizations.of(context)!.addEntry) + ], + ), ), ), ) : null, From 02709cc0ea7bebc65055533cc4688e7d7686e305 Mon Sep 17 00:00:00 2001 From: derdilla Date: Tue, 20 Jun 2023 09:38:56 +0200 Subject: [PATCH 15/35] make headline optional --- lib/l10n/app_de.arb | 3 +++ lib/l10n/app_en.arb | 2 ++ lib/model/export_import.dart | 12 +++++++----- lib/model/settings_store.dart | 9 ++++++++- lib/screens/subsettings/export_import_screen.dart | 8 ++++++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8b9bdd29..9d6407c7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -85,6 +85,9 @@ "exportCustomEntries": "Eigene Felder", "addEntry": "Feld hinzufügen", "exportMimeType": "Export MIME typ", + "exportMimeTypeDesc": "gibt anderen dateityp weiter", + "exportCsvHeadline": "Überschrift", + "exportCsvHeadlineDesc": "Feldbezeichnungen zum Differenzieren", "csv": "CSV", "pdf": "PDF", "text": "Text", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 53687117..10183047 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -86,6 +86,8 @@ "addEntry": "Feld hinzufügen", "exportMimeType": "export MIME type", "exportMimeTypeDesc": "signalizes type to other apps", + "exportCsvHeadline": "headline", + "exportCsvHeadlineDesc": "Helps to discriminate types", "csv": "CSV", "pdf": "PDF", "text": "text", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 4d135899..acb3bec8 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -22,13 +22,15 @@ class DataExporter { } var csvHead = ''; - for (var i = 0; i> items = []; for (var record in records) { diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 458410e0..7e36d451 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -384,8 +384,15 @@ class Settings extends ChangeNotifier { _prefs.setStringList('exportItems', value); notifyListeners(); } - + bool get exportCsvHeadline { + return _prefs.getBool('exportCsvHeadline') ?? true; + } + + set exportCsvHeadline(bool value) { + _prefs.setBool('exportCsvHeadline', value); + notifyListeners(); + } } class TimeStep { diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index d2193359..a2f3e5f1 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -58,6 +58,14 @@ class ExportImportScreen extends StatelessWidget { } }, ), + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportCsvHeadline), + description: Text(AppLocalizations.of(context)!.exportCsvHeadlineDesc), + initialValue: settings.exportCsvHeadline, + onToggle: (value) { + settings.exportCsvHeadline = value; + } + ), SwitchSettingsTile( title: Text(AppLocalizations.of(context)!.exportCustomEntries), initialValue: settings.exportCustomEntries, From e8d3a52b413bc2eb5a2683ba218398501b6743b0 Mon Sep 17 00:00:00 2001 From: derdilla Date: Tue, 20 Jun 2023 18:15:06 +0200 Subject: [PATCH 16/35] rewrite function to parse csv files --- lib/l10n/app_de.arb | 3 + lib/l10n/app_en.arb | 3 + lib/model/export_import.dart | 62 ++++++++++++++++++- .../subsettings/export_import_screen.dart | 46 +++++++++----- 4 files changed, 97 insertions(+), 17 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9d6407c7..16b8147f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -37,6 +37,9 @@ "errNoData": "Keine Daten", "errNoRangeForExport": "Sie müssen angeben, welche daten sie exportieren wollen.", "errPleaseSelect": "Bitte auswählen", + "errNotCsvFormat": "Es können nur Dateien im csv Format importiert werden.", + "errNeedHeadline": "Es können nur Dateien mit einer Überschrift importiert werden.", + "btnCancel": "ABBRUCH", "btnSave": "OK", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 10183047..851b0fc1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -37,6 +37,9 @@ "errNoData": "no data", "errNoRangeForExport": "You need to specify a range in which data is exported.", "errPleaseSelect": "please select", + "errNotCsvFormat": "You can only import files in csv format.", + "errNeedHeadline": "You can only import files with a headline.", + "errCantReadFile": "The file contents can not be read", "btnCancel": "CANCEL", "btnSave": "SAVE", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index acb3bec8..fc23b6b4 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -29,7 +29,7 @@ class DataExporter { csvHead += settings.csvFieldDelimiter; } } - csvHead += '\n'; + csvHead += '\r\n'; } List> items = []; @@ -64,6 +64,66 @@ class DataExporter { } return Uint8List(0); } + + List parseCSVFile(Uint8List data) { + assert(settings.exportFormat == ExportFormat.csv); + assert(settings.exportCsvHeadline); + + List records = []; + + String fileContents = utf8.decode(data.toList()); + final converter = CsvToListConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter); + final csvLines = converter.convert(fileContents); + if (csvLines.length <= 1) { + throw const FormatException('empty file'); + } + final attributes = csvLines.removeAt(0); + var creationTimePos = -1; + var sysPos = -1; + var diaPos = -1; + var pulPos = -1; + var notePos = -1; + for (var i = 0; i= 0); + assert(sysPos >= 0); + assert(diaPos >= 0); + assert(pulPos >= 0); + assert(notePos >= 0); + + for (final line in csvLines) { + records.add( + BloodPressureRecord( + DateTime.fromMillisecondsSinceEpoch(line[creationTimePos]), + line[sysPos], + line[diaPos], + line[pulPos], + line[notePos] + ) + ); + } + // TODO: maybe use customized fields if no header is present? + // requires changes in screen class + + return records; + } } class ExportFormat { diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index a2f3e5f1..71251c41 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -6,6 +6,7 @@ import 'package:blood_pressure_app/components/settings_widgets.dart'; import 'package:blood_pressure_app/model/blood_pressure.dart'; import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -194,23 +195,36 @@ class ExportImportScreen extends StatelessWidget { child: MaterialButton( height: 60, child: Text(AppLocalizations.of(context)!.import), - onPressed: () { - try { - Provider.of(context, listen: false).import((res) { - if (res) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: - Text(AppLocalizations.of(context)!.success(AppLocalizations.of(context)!.import)))); - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)! - .error(AppLocalizations.of(context)!.errNoFileOpened)))); - } - }); - } on Exception catch (e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.error(e.toString())))); + onPressed: () async { + final settings = Provider.of(context, listen: false); + if (!(settings.exportFormat == ExportFormat.csv)) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNotCsvFormat))); + } + if (!settings.exportCsvHeadline) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNeedHeadline))); + } + + // TODO: import from here + + + var result = await FilePicker.platform.pickFiles( + allowMultiple: false, + withData: true, + ); + if (result == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNoFileOpened))); + return; + } + var binaryContent = result.files.single.bytes; + if (binaryContent == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errCantReadFile))); + return; } + DataExporter(settings).parseCSVFile(binaryContent); }, ) ), From ea253eab048ccb1056bee282d5e6f230b827162c Mon Sep 17 00:00:00 2001 From: derdilla Date: Tue, 20 Jun 2023 18:32:37 +0200 Subject: [PATCH 17/35] rewrite import --- lib/l10n/app_en.arb | 8 ++++++++ lib/model/export_import.dart | 6 ------ lib/screens/subsettings/export_import_screen.dart | 14 ++++++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 851b0fc1..5620d476 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -110,6 +110,14 @@ "import": "IMPORT", "sourceCode": "source code", "licenses": "3rd party licenses", + "importSuccess": "Successfully imported {count} entries", + "@importSuccess": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "statistics": "Statistics", "measurementCount": "Measurement count", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index fc23b6b4..90eb8c73 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -74,9 +74,6 @@ class DataExporter { String fileContents = utf8.decode(data.toList()); final converter = CsvToListConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter); final csvLines = converter.convert(fileContents); - if (csvLines.length <= 1) { - throw const FormatException('empty file'); - } final attributes = csvLines.removeAt(0); var creationTimePos = -1; var sysPos = -1; @@ -119,9 +116,6 @@ class DataExporter { ) ); } - // TODO: maybe use customized fields if no header is present? - // requires changes in screen class - return records; } } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 71251c41..a8a934d0 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -206,13 +206,11 @@ class ExportImportScreen extends StatelessWidget { content: Text(AppLocalizations.of(context)!.errNeedHeadline))); } - // TODO: import from here - - var result = await FilePicker.platform.pickFiles( allowMultiple: false, withData: true, ); + if (!context.mounted) return; if (result == null) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.errNoFileOpened))); @@ -224,7 +222,15 @@ class ExportImportScreen extends StatelessWidget { content: Text(AppLocalizations.of(context)!.errCantReadFile))); return; } - DataExporter(settings).parseCSVFile(binaryContent); + + var fileContents = DataExporter(settings).parseCSVFile(binaryContent); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length)))); + var model = Provider.of(context, listen: false); + for (final e in fileContents) { + model.add(e); + } + }, ) ), From a18d942b6f270c56ee6f0696d48f79c779855315 Mon Sep 17 00:00:00 2001 From: derdilla Date: Tue, 20 Jun 2023 18:36:04 +0200 Subject: [PATCH 18/35] remove old model method --- lib/model/blood_pressure.dart | 62 ------------------------- lib/model/ram_only_implementations.dart | 12 ----- test/model/bood_pressure_test.dart | 11 ----- 3 files changed, 85 deletions(-) diff --git a/lib/model/blood_pressure.dart b/lib/model/blood_pressure.dart index 301e8099..5f8aa3ac 100644 --- a/lib/model/blood_pressure.dart +++ b/lib/model/blood_pressure.dart @@ -1,13 +1,8 @@ -import 'dart:convert' show utf8; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:csv/csv.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:file_saver/file_saver.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; -import 'package:share_plus/share_plus.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class BloodPressureModel extends ChangeNotifier { @@ -156,63 +151,6 @@ class BloodPressureModel extends ChangeNotifier { return (res as int?) ?? -1; } - Future save(void Function(bool success, String? msg) callback, {bool exportAsText = false}) async { - // create csv - String csvData = 'timestampUnixMs, systolic, diastolic, pulse, notes\n'; - List> allEntries = await _database.query('bloodPressureModel', orderBy: 'timestamp DESC'); - List> data = []; - for (var e in allEntries) { - data.add([e['timestamp'],e['systolic'], e['diastolic'], e['pulse'], e['notes']]); - } - csvData += const ListToCsvConverter().convert(data, delimitAllFields: true); - - // save data - String filename = 'blood_press_${DateTime.now().toIso8601String()}'; - String path = await FileSaver.instance - .saveFile(name: filename, bytes: Uint8List.fromList(utf8.encode(csvData)), ext: 'csv', mimeType: MimeType.csv); - - - // notify user about location - if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - callback(true, path); - } else if (Platform.isAndroid || Platform.isIOS) { - var mimeType = MimeType.csv; - if (exportAsText) { - mimeType = MimeType.text; - } - Share.shareXFiles([ - XFile( - path, - mimeType: mimeType.type, - ) - ]); - callback(true, null); - } else {} - } - - Future import(void Function(bool) callback) async { - var result = await FilePicker.platform.pickFiles( - allowMultiple: false, - withData: true, - ); - - if (result != null) { - var binaryContent = result.files.single.bytes; - if (binaryContent != null) { - final csvContents = const CsvToListConverter() - .convert(utf8.decode(binaryContent), fieldDelimiter: ',', textDelimiter: '"', eol: '\n'); - for (var i = 1; i < csvContents.length; i++) { - var line = csvContents[i]; - BloodPressureRecord record = BloodPressureRecord(DateTime.fromMillisecondsSinceEpoch(line[0] as int), - (line[1] as int), (line[2] as int), (line[3] as int), line[4].toString()); - add(record); - } - return callback(true); - } - } - return callback(false); - } - void close() { _database.close(); } diff --git a/lib/model/ram_only_implementations.dart b/lib/model/ram_only_implementations.dart index 5fe2fdec..2683b93f 100644 --- a/lib/model/ram_only_implementations.dart +++ b/lib/model/ram_only_implementations.dart @@ -77,18 +77,6 @@ class RamBloodPressureModel extends ChangeNotifier implements BloodPressureModel @override void close() {} - - @override - Future import(void Function(bool p1) callback) { - // TODO: implement import - throw UnimplementedError(); - } - - @override - Future save(void Function(bool success, String? msg) callback, {bool exportAsText = false}) { - // TODO: implement save - throw UnimplementedError(); - } } class RamSettings extends ChangeNotifier implements Settings { diff --git a/test/model/bood_pressure_test.dart b/test/model/bood_pressure_test.dart index c4c709c6..e3e56f11 100644 --- a/test/model/bood_pressure_test.dart +++ b/test/model/bood_pressure_test.dart @@ -83,17 +83,6 @@ void main() { expect(res.notes, r.notes); }); - test('should import exported values', () async { - var m = await BloodPressureModel.create(dbPath: '/tmp/bp_test/should_import_exported'); - var r = BloodPressureRecord(DateTime.fromMillisecondsSinceEpoch(31415926), -172, 10000, 0, - "((V⍳V)=⍳⍴V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈๏ แผ่นดินฮั่นเABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ–—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвг, \n \t д∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა"); - await m.add(r); - - m.save((success, msg) { - expect(success, true); - }); - }); - test('should delete', () async { var m = await BloodPressureModel.create(dbPath: '/tmp/bp_test/should_delete'); From 642b80322ed90525e916d0f1d7125b2558d69f3e Mon Sep 17 00:00:00 2001 From: derdilla Date: Wed, 21 Jun 2023 15:36:01 +0200 Subject: [PATCH 19/35] fix tests andwarnings --- lib/model/ram_only_implementations.dart | 107 ++++++++++++++++++++++-- lib/model/settings_store.dart | 4 - test/model/settings_test.dart | 74 +++++++++++++--- 3 files changed, 162 insertions(+), 23 deletions(-) diff --git a/lib/model/ram_only_implementations.dart b/lib/model/ram_only_implementations.dart index 2683b93f..f2c0b60d 100644 --- a/lib/model/ram_only_implementations.dart +++ b/lib/model/ram_only_implementations.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:blood_pressure_app/model/blood_pressure.dart'; import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; +import 'package:file_saver/src/utils/mime_types.dart'; import 'package:flutter/material.dart'; class RamBloodPressureModel extends ChangeNotifier implements BloodPressureModel { @@ -103,6 +104,15 @@ class RamSettings extends ChangeNotifier implements Settings { bool _validateInputs = true; int _graphTitlesCount = 5; ExportFormat _exportFormat = ExportFormat.csv; + String _csvFieldDelimiter = ','; + String _csvTextDelimiter = '"'; + List _exportAddableItems = []; + bool _exportCsvHeadline = true; + bool _exportCustomEntries = false; + DateTimeRange _exportDataRange = DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(0), end: DateTime.fromMillisecondsSinceEpoch(0)); + List _exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; + bool _exportLimitDataRange = false; + MimeType _exportMimeType = MimeType.csv; RamSettings() { _accentColor = createMaterialColor(0xFF009688); @@ -296,15 +306,6 @@ class RamSettings extends ChangeNotifier implements Settings { notifyListeners(); } - @override - bool get useExportCompatability => _useExportCompatability; - - @override - set useExportCompatability(bool value) { - _useExportCompatability = value; - notifyListeners(); - } - @override bool get validateInputs => _validateInputs; @@ -332,6 +333,94 @@ class RamSettings extends ChangeNotifier implements Settings { notifyListeners(); } + bool get useExportCompatability => _useExportCompatability; + + set useExportCompatability(bool value) { + _useExportCompatability = value; + notifyListeners(); + } + + @override + String get csvFieldDelimiter => _csvFieldDelimiter; + + @override + set csvFieldDelimiter(String value) { + _csvFieldDelimiter = value; + notifyListeners(); + } + + @override + String get csvTextDelimiter => _csvTextDelimiter; + + @override + set csvTextDelimiter(String value) { + _csvTextDelimiter = value; + notifyListeners(); + } + + @override + List get exportAddableItems => _exportAddableItems; + + @override + set exportAddableItems(List value) { + _exportAddableItems = value; + notifyListeners(); + } + + @override + bool get exportCsvHeadline => _exportCsvHeadline; + + @override + set exportCsvHeadline(bool value) { + _exportCsvHeadline = value; + notifyListeners(); + } + + @override + bool get exportCustomEntries => _exportCustomEntries; + + @override + set exportCustomEntries(bool value) { + _exportCustomEntries = value; + notifyListeners(); + } + + @override + DateTimeRange get exportDataRange => _exportDataRange; + + @override + set exportDataRange(DateTimeRange value) { + _exportDataRange = value; + notifyListeners(); + } + + @override + List get exportItems => _exportItems; + + @override + set exportItems(List value) { + _exportItems = value; + notifyListeners(); + } + + @override + bool get exportLimitDataRange => _exportLimitDataRange; + + @override + set exportLimitDataRange(bool value) { + _exportLimitDataRange = value; + notifyListeners(); + } + + @override + MimeType get exportMimeType => _exportMimeType; + + @override + set exportMimeType(MimeType value) { + _exportMimeType = value; + notifyListeners(); + } + @override void changeStepSize(int value) { graphStepSize = value; diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 7e36d451..186387e9 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -19,10 +19,6 @@ class Settings extends ChangeNotifier { return component; } - void forceNotifyListeners() { - notifyListeners(); - } - int get graphStepSize { return _prefs.getInt('graphStepSize') ?? TimeStep.day; } diff --git a/test/model/settings_test.dart b/test/model/settings_test.dart index a524769b..6e515b4a 100644 --- a/test/model/settings_test.dart +++ b/test/model/settings_test.dart @@ -1,5 +1,7 @@ import 'package:blood_pressure_app/model/ram_only_implementations.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; +import 'package:file_saver/file_saver.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; @@ -29,7 +31,6 @@ void main() { expect(s.pulColor.value, 0xFFF44336); expect(s.allowManualTimeInput, true); expect(s.dateFormatString, 'yyyy-MM-dd HH:mm'); - expect(s.useExportCompatability, false); expect(s.iconSize, 30); expect(s.sysWarn, 125); // depends on overrideWarnValues expect(s.diaWarn, 80); // depends on overrideWarnValues @@ -40,6 +41,14 @@ void main() { expect(s.animationSpeed, 150); expect(s.confirmDeletion, true); expect(s.graphTitlesCount, 5); + expect(s.csvFieldDelimiter, ','); + expect(s.csvTextDelimiter, '"'); + expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportAddableItems, []); + expect(s.exportCsvHeadline, true); + expect(s.exportDataRange.start.millisecondsSinceEpoch, 0); + expect(s.exportLimitDataRange, false); + expect(s.exportMimeType, MimeType.csv); s.overrideWarnValues = true; expect(s.sysWarn, 120); @@ -70,7 +79,6 @@ void main() { s.pulColor = createMaterialColor(0xFF942DA7); s.allowManualTimeInput = false; s.dateFormatString = 'yy:dd @ H:mm.ss'; - s.useExportCompatability = true; s.iconSize = 50; s.sysWarn = 314; // depends on overrideWarnValues s.diaWarn = 159; // depends on overrideWarnValues @@ -81,6 +89,13 @@ void main() { s.animationSpeed = 100; s.confirmDeletion = false; s.graphTitlesCount = 7; + s.csvFieldDelimiter = '|'; + s.csvTextDelimiter = '\''; + s.exportAddableItems = ['timestampUnixMs']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportCsvHeadline = false; + s.exportLimitDataRange = true; + s.exportMimeType = MimeType.pdf; expect(s.displayDataStart, DateTime.fromMillisecondsSinceEpoch(10000)); expect(s.displayDataEnd, DateTime.fromMillisecondsSinceEpoch(200000)); @@ -91,7 +106,6 @@ void main() { expect(s.diaColor.value, 0xFF942DA6); expect(s.pulColor.value, 0xFF942DA7); expect(s.allowManualTimeInput, false); - expect(s.useExportCompatability, true); expect(s.iconSize, 50); expect(s.sysWarn, 314); expect(s.diaWarn, 159); @@ -102,6 +116,13 @@ void main() { expect(s.animationSpeed, 100); expect(s.confirmDeletion, false); expect(s.graphTitlesCount, 7); + expect(s.csvFieldDelimiter, '|'); + expect(s.csvTextDelimiter, '\''); + expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportAddableItems, ['timestampUnixMs']); + expect(s.exportCsvHeadline, false); + expect(s.exportLimitDataRange, true); + expect(s.exportMimeType, MimeType.pdf); }); test('setting fields should notify listeners and change values', () async { @@ -123,7 +144,6 @@ void main() { s.pulColor = createMaterialColor(0xFF942DA7); s.allowManualTimeInput = false; s.dateFormatString = 'yy:dd @ H:mm.ss'; - s.useExportCompatability = true; s.iconSize = 10; s.sysWarn = 314; // depends on overrideWarnValues s.diaWarn = 159; // depends on overrideWarnValues @@ -134,8 +154,16 @@ void main() { s.animationSpeed = 100; s.confirmDeletion = true; s.graphTitlesCount = 2; + s.csvFieldDelimiter = '|'; + s.csvTextDelimiter = '\''; + s.exportAddableItems = ['timestampUnixMs']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportCsvHeadline = false; + s.exportDataRange = DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(20), end: DateTime.now()); + s.exportLimitDataRange = true; + s.exportMimeType = MimeType.pdf; - expect(i, 22); + expect(i, 29); }); }); @@ -160,7 +188,6 @@ void main() { expect(s.pulColor.value, 0xFFF44336); expect(s.allowManualTimeInput, true); expect(s.dateFormatString, 'yyyy-MM-dd HH:mm'); - expect(s.useExportCompatability, false); expect(s.iconSize, 30); expect(s.sysWarn, 125); // depends on overrideWarnValues expect(s.diaWarn, 80); // depends on overrideWarnValues @@ -171,6 +198,14 @@ void main() { expect(s.animationSpeed, 150); expect(s.confirmDeletion, true); expect(s.graphTitlesCount, 5); + expect(s.csvFieldDelimiter, ','); + expect(s.csvTextDelimiter, '"'); + expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportAddableItems, []); + expect(s.exportCsvHeadline, true); + expect(s.exportDataRange.start.millisecondsSinceEpoch, 0); + expect(s.exportLimitDataRange, false); + expect(s.exportMimeType, MimeType.csv); s.overrideWarnValues = true; expect(s.sysWarn, 120); @@ -201,7 +236,6 @@ void main() { s.pulColor = createMaterialColor(0xFF942DA7); s.allowManualTimeInput = false; s.dateFormatString = 'yy:dd @ H:mm.ss'; - s.useExportCompatability = true; s.iconSize = 50; s.sysWarn = 314; // depends on overrideWarnValues s.diaWarn = 159; // depends on overrideWarnValues @@ -212,6 +246,13 @@ void main() { s.animationSpeed = 100; s.confirmDeletion = false; s.graphTitlesCount = 7; + s.csvFieldDelimiter = '|'; + s.csvTextDelimiter = '\''; + s.exportAddableItems = ['timestampUnixMs']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportCsvHeadline = false; + s.exportLimitDataRange = true; + s.exportMimeType = MimeType.pdf; expect(s.displayDataStart, DateTime.fromMillisecondsSinceEpoch(10000)); expect(s.displayDataEnd, DateTime.fromMillisecondsSinceEpoch(200000)); @@ -222,7 +263,6 @@ void main() { expect(s.diaColor.value, 0xFF942DA6); expect(s.pulColor.value, 0xFF942DA7); expect(s.allowManualTimeInput, false); - expect(s.useExportCompatability, true); expect(s.iconSize, 50); expect(s.sysWarn, 314); expect(s.diaWarn, 159); @@ -233,6 +273,13 @@ void main() { expect(s.animationSpeed, 100); expect(s.confirmDeletion, false); expect(s.graphTitlesCount, 7); + expect(s.csvFieldDelimiter, '|'); + expect(s.csvTextDelimiter, '\''); + expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportAddableItems, ['timestampUnixMs']); + expect(s.exportCsvHeadline, false); + expect(s.exportLimitDataRange, true); + expect(s.exportMimeType, MimeType.pdf); }); test('setting fields should notify listeners and change values', () async { @@ -254,7 +301,6 @@ void main() { s.pulColor = createMaterialColor(0xFF942DA7); s.allowManualTimeInput = false; s.dateFormatString = 'yy:dd @ H:mm.ss'; - s.useExportCompatability = true; s.iconSize = 10; s.sysWarn = 314; // depends on overrideWarnValues s.diaWarn = 159; // depends on overrideWarnValues @@ -265,8 +311,16 @@ void main() { s.animationSpeed = 100; s.confirmDeletion = true; s.graphTitlesCount = 2; + s.csvFieldDelimiter = '|'; + s.csvTextDelimiter = '\''; + s.exportAddableItems = ['timestampUnixMs']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportCsvHeadline = false; + s.exportDataRange = DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(20), end: DateTime.now()); + s.exportLimitDataRange = true; + s.exportMimeType = MimeType.pdf; - expect(i, 22); + expect(i, 29); }); }); } From 299e9fa095f4fe220947d006e35383f5e5dc97f9 Mon Sep 17 00:00:00 2001 From: derdilla Date: Wed, 21 Jun 2023 18:06:27 +0200 Subject: [PATCH 20/35] add more option to export in iso format --- lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/model/export_import.dart | 11 +++++++-- lib/model/ram_only_implementations.dart | 2 +- lib/model/settings_store.dart | 2 +- .../subsettings/export_import_screen.dart | 23 +++++++++++-------- test/model/settings_test.dart | 16 ++++++------- 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 16b8147f..2a59aca1 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -39,6 +39,7 @@ "errPleaseSelect": "Bitte auswählen", "errNotCsvFormat": "Es können nur Dateien im csv Format importiert werden.", "errNeedHeadline": "Es können nur Dateien mit einer Überschrift importiert werden.", + "errNotImportable": "Diese Datei kann nicht importiert werden.", "btnCancel": "ABBRUCH", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5620d476..49b01bf6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -40,6 +40,7 @@ "errNotCsvFormat": "You can only import files in csv format.", "errNeedHeadline": "You can only import files with a headline.", "errCantReadFile": "The file contents can not be read", + "errNotImportable": "This file can't be imported", "btnCancel": "CANCEL", "btnSave": "SAVE", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 90eb8c73..1778a5ce 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -40,6 +40,9 @@ class DataExporter { case 'timestampUnixMs': row.add(record.creationTime.millisecondsSinceEpoch); break; + case 'isoUTCTime': + row.add(record.creationTime.toIso8601String()); + break; case 'systolic': row.add(record.systolic); break; @@ -76,6 +79,7 @@ class DataExporter { final csvLines = converter.convert(fileContents); final attributes = csvLines.removeAt(0); var creationTimePos = -1; + var isoTimePos = -1; var sysPos = -1; var diaPos = -1; var pulPos = -1; @@ -85,6 +89,9 @@ class DataExporter { case 'timestampUnixMs': creationTimePos = i; break; + case 'isoUTCTime': + isoTimePos = i; + break; case 'systolic': sysPos = i; break; @@ -99,7 +106,7 @@ class DataExporter { break; } } - assert(creationTimePos >= 0); + assert(creationTimePos >= 0 || isoTimePos >= 0); assert(sysPos >= 0); assert(diaPos >= 0); assert(pulPos >= 0); @@ -108,7 +115,7 @@ class DataExporter { for (final line in csvLines) { records.add( BloodPressureRecord( - DateTime.fromMillisecondsSinceEpoch(line[creationTimePos]), + (creationTimePos >= 0 ) ? DateTime.fromMillisecondsSinceEpoch(line[creationTimePos]) : DateTime.parse(line[isoTimePos]), line[sysPos], line[diaPos], line[pulPos], diff --git a/lib/model/ram_only_implementations.dart b/lib/model/ram_only_implementations.dart index f2c0b60d..17fd5bad 100644 --- a/lib/model/ram_only_implementations.dart +++ b/lib/model/ram_only_implementations.dart @@ -106,7 +106,7 @@ class RamSettings extends ChangeNotifier implements Settings { ExportFormat _exportFormat = ExportFormat.csv; String _csvFieldDelimiter = ','; String _csvTextDelimiter = '"'; - List _exportAddableItems = []; + List _exportAddableItems = ['isoUTCTime']; bool _exportCsvHeadline = true; bool _exportCustomEntries = false; DateTimeRange _exportDataRange = DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(0), end: DateTime.fromMillisecondsSinceEpoch(0)); diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 186387e9..a92882f3 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -365,7 +365,7 @@ class Settings extends ChangeNotifier { } List get exportAddableItems { - return _prefs.getStringList('exportAddableItems') ?? []; + return _prefs.getStringList('exportAddableItems') ?? ['isoUTCTime']; } set exportAddableItems(List value) { diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index a8a934d0..fde22b52 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -14,6 +14,7 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; +// FIXME: Export/import buttons overlap with content on snack bar class ExportImportScreen extends StatelessWidget { const ExportImportScreen({super.key}); @@ -222,15 +223,19 @@ class ExportImportScreen extends StatelessWidget { content: Text(AppLocalizations.of(context)!.errCantReadFile))); return; } - - var fileContents = DataExporter(settings).parseCSVFile(binaryContent); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length)))); - var model = Provider.of(context, listen: false); - for (final e in fileContents) { - model.add(e); - } + try { + var fileContents = DataExporter(settings).parseCSVFile(binaryContent); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length)))); + var model = Provider.of(context, listen: false); + for (final e in fileContents) { + model.add(e); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNotImportable))); + } }, ) ), @@ -270,7 +275,7 @@ class CsvItemsOrderCreator extends StatelessWidget { settings.exportItems = exportItems; }, - footer: (settings.exportAddableItems.isNotEmpty) ? InkWell( + footer: (settings.exportItems.length < 5) ? InkWell( onTap: () async { await showDialog(context: context, builder: (context) { diff --git a/test/model/settings_test.dart b/test/model/settings_test.dart index 6e515b4a..95939983 100644 --- a/test/model/settings_test.dart +++ b/test/model/settings_test.dart @@ -44,7 +44,7 @@ void main() { expect(s.csvFieldDelimiter, ','); expect(s.csvTextDelimiter, '"'); expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); - expect(s.exportAddableItems, []); + expect(s.exportAddableItems, ['isoUTCTime']); expect(s.exportCsvHeadline, true); expect(s.exportDataRange.start.millisecondsSinceEpoch, 0); expect(s.exportLimitDataRange, false); @@ -92,7 +92,7 @@ void main() { s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; s.exportAddableItems = ['timestampUnixMs']; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportLimitDataRange = true; s.exportMimeType = MimeType.pdf; @@ -118,7 +118,7 @@ void main() { expect(s.graphTitlesCount, 7); expect(s.csvFieldDelimiter, '|'); expect(s.csvTextDelimiter, '\''); - expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']); expect(s.exportAddableItems, ['timestampUnixMs']); expect(s.exportCsvHeadline, false); expect(s.exportLimitDataRange, true); @@ -157,7 +157,7 @@ void main() { s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; s.exportAddableItems = ['timestampUnixMs']; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportDataRange = DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(20), end: DateTime.now()); s.exportLimitDataRange = true; @@ -201,7 +201,7 @@ void main() { expect(s.csvFieldDelimiter, ','); expect(s.csvTextDelimiter, '"'); expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); - expect(s.exportAddableItems, []); + expect(s.exportAddableItems, ['isoUTCTime']); expect(s.exportCsvHeadline, true); expect(s.exportDataRange.start.millisecondsSinceEpoch, 0); expect(s.exportLimitDataRange, false); @@ -249,7 +249,7 @@ void main() { s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; s.exportAddableItems = ['timestampUnixMs']; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportLimitDataRange = true; s.exportMimeType = MimeType.pdf; @@ -275,7 +275,7 @@ void main() { expect(s.graphTitlesCount, 7); expect(s.csvFieldDelimiter, '|'); expect(s.csvTextDelimiter, '\''); - expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']); expect(s.exportAddableItems, ['timestampUnixMs']); expect(s.exportCsvHeadline, false); expect(s.exportLimitDataRange, true); @@ -314,7 +314,7 @@ void main() { s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; s.exportAddableItems = ['timestampUnixMs']; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes']; + s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportDataRange = DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(20), end: DateTime.now()); s.exportLimitDataRange = true; From 406df242858ccb14a5a6e31ec1a407bfa4c4e307 Mon Sep 17 00:00:00 2001 From: derdilla Date: Wed, 21 Jun 2023 18:11:43 +0200 Subject: [PATCH 21/35] fix bottom button overlapping on snack bar --- lib/screens/subsettings/export_import_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index fde22b52..c474568e 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -14,7 +14,6 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; -// FIXME: Export/import buttons overlap with content on snack bar class ExportImportScreen extends StatelessWidget { const ExportImportScreen({super.key}); @@ -143,8 +142,9 @@ class ExportImportScreen extends StatelessWidget { ); }), ), - floatingActionButton: SizedBox( + floatingActionButton: Container( height: 60, + color: Theme.of(context).colorScheme.onBackground, child: Center( child: Row( children: [ From 76a6894ed80075a2b6ed15faa60085f92a918499 Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 10:29:21 +0200 Subject: [PATCH 22/35] Add warn banner for non-importable configuration --- lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 1 + .../subsettings/export_import_screen.dart | 124 ++++++++++++------ 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2a59aca1..a63e1f5f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -107,6 +107,8 @@ } } }, + "exportWarnConfigNotImportable": "Hey! Nur eine freundliche Info: Die aktuelle Exportkonfiguration ist nicht importierbar. Um das zu beheben, stelle sicher, dass der Exporttyp als CSV festgelegt ist, die Überschrift aktiviert ist und die Felder 'diastolic', 'systolic', 'pulse', 'notes' sowie eines der verfügbaren Zeitformate enthalten sind.", + "shared": "Geteilt", "import": "IMPORT", "sourceCode": "Quellcode", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 49b01bf6..5a12a23c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -119,6 +119,7 @@ } } }, + "exportWarnConfigNotImportable": "Hey! Just a friendly heads up: the current export configuration won't be importable. To fix it, make sure you set the export type as CSV, enable the headline, and include the fields 'diastolic', 'systolic', 'pulse', 'notes', along with one of the time formats available.", "statistics": "Statistics", "measurementCount": "Measurement count", diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index c474568e..e60b1572 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -14,9 +14,16 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; -class ExportImportScreen extends StatelessWidget { +class ExportImportScreen extends StatefulWidget { const ExportImportScreen({super.key}); + @override + State createState() => _ExportImportScreenState(); +} + +class _ExportImportScreenState extends State { + bool _showWarnBanner = true; + @override Widget build(BuildContext context) { return Scaffold( @@ -27,6 +34,7 @@ class ExportImportScreen extends StatelessWidget { body: Container( margin: const EdgeInsets.only(bottom: 80), child: Consumer(builder: (context, settings, child) { + // export range var exportRange = settings.exportDataRange; String exportRangeText; if (exportRange.start.millisecondsSinceEpoch != 0 && exportRange.end.millisecondsSinceEpoch != 0) { @@ -36,48 +44,7 @@ class ExportImportScreen extends StatelessWidget { exportRangeText = AppLocalizations.of(context)!.errPleaseSelect; } - List modeSpecificSettings = []; - if (settings.exportFormat == ExportFormat.csv) { - modeSpecificSettings = [ - InputSettingsTile( - title: Text(AppLocalizations.of(context)!.fieldDelimiter), - inputWidth: 40, - initialValue: settings.csvFieldDelimiter, - onEditingComplete: (value) { - if (value != null) { - settings.csvFieldDelimiter = value; - } - }, - ), - InputSettingsTile( - title: Text(AppLocalizations.of(context)!.textDelimiter), - inputWidth: 40, - initialValue: settings.csvTextDelimiter, - onEditingComplete: (value) { - if (value != null) { - settings.csvTextDelimiter = value; - } - }, - ), - SwitchSettingsTile( - title: Text(AppLocalizations.of(context)!.exportCsvHeadline), - description: Text(AppLocalizations.of(context)!.exportCsvHeadlineDesc), - initialValue: settings.exportCsvHeadline, - onToggle: (value) { - settings.exportCsvHeadline = value; - } - ), - SwitchSettingsTile( - title: Text(AppLocalizations.of(context)!.exportCustomEntries), - initialValue: settings.exportCustomEntries, - onToggle: (value) { - settings.exportCustomEntries = value; - } - ), - (settings.exportCustomEntries) ? const CsvItemsOrderCreator(): const SizedBox.shrink() - ]; - } - + // default options List options = [ SwitchSettingsTile( title: Text(AppLocalizations.of(context)!.exportLimitDataRange), @@ -135,6 +102,77 @@ class ExportImportScreen extends StatelessWidget { ), */ ]; + + // mode specifics + List modeSpecificSettings = []; + if (settings.exportFormat == ExportFormat.csv) { + modeSpecificSettings = [ + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.fieldDelimiter), + inputWidth: 40, + initialValue: settings.csvFieldDelimiter, + onEditingComplete: (value) { + if (value != null) { + settings.csvFieldDelimiter = value; + } + }, + ), + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.textDelimiter), + inputWidth: 40, + initialValue: settings.csvTextDelimiter, + onEditingComplete: (value) { + if (value != null) { + settings.csvTextDelimiter = value; + } + }, + ), + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportCsvHeadline), + description: Text(AppLocalizations.of(context)!.exportCsvHeadlineDesc), + initialValue: settings.exportCsvHeadline, + onToggle: (value) { + settings.exportCsvHeadline = value; + } + ), + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportCustomEntries), + initialValue: settings.exportCustomEntries, + onToggle: (value) { + settings.exportCustomEntries = value; + } + ), + (settings.exportCustomEntries) ? const CsvItemsOrderCreator(): const SizedBox.shrink() + ]; + } + + // warn banner when + if (_showWarnBanner && (settings.exportFormat != ExportFormat.csv || + settings.exportCsvHeadline == false || + !( + (settings.exportItems.contains('timestampUnixMs') || settings.exportItems.contains('isoUTCTime')) && + settings.exportItems.contains('systolic') && + settings.exportItems.contains('diastolic') && + settings.exportItems.contains('pulse') && + settings.exportItems.contains('notes') + ) + )) { + options.insert(0, MaterialBanner( + padding: const EdgeInsets.all(20), + content: Text(AppLocalizations.of(context)!.exportWarnConfigNotImportable), + actions: [ + TextButton( + onPressed: () { + setState(() { + _showWarnBanner = false; + }); + }, + child: Text(AppLocalizations.of(context)!.btnConfirm)) + ] + )); + } + + // create view options.addAll(modeSpecificSettings); options.add(const SizedBox(height: 20,)); return ListView( From f972010748280c31c73c7b5576a05a957f625833 Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 10:36:28 +0200 Subject: [PATCH 23/35] migrate ExportFormat to enum --- lib/model/export_import.dart | 27 +++------------------------ lib/model/settings_store.dart | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 1778a5ce..d8d59de6 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -127,28 +127,7 @@ class DataExporter { } } -class ExportFormat { - final int code; - - ExportFormat(this.code) { - if (code < 0 || code > 1) throw const FormatException('Not a export format'); - } - static ExportFormat csv = ExportFormat(0); - static ExportFormat pdf = ExportFormat(1); - - @override - bool operator == (Object other) { - try { - return code == (other as ExportFormat).code; - } on Exception { - try { - return code == (other as int); - } on Exception { - return false; - } - } - } - - @override - int get hashCode => code; +enum ExportFormat { + csv, + pdf } \ No newline at end of file diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index a92882f3..142abb5d 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -274,11 +274,28 @@ class Settings extends ChangeNotifier { } ExportFormat get exportFormat { - return ExportFormat(_prefs.getInt('exportFormat') ?? 0); + switch (_prefs.getInt('exportFormat') ?? 0) { + case 0: + return ExportFormat.csv; + case 1: + return ExportFormat.pdf; + default: + assert(false); + return ExportFormat.csv; + } } set exportFormat(ExportFormat format) { - _prefs.setInt('exportFormat', format.code); + switch (format) { + case ExportFormat.csv: + _prefs.setInt('exportFormat', 0); + break; + case ExportFormat.pdf: + _prefs.setInt('exportFormat', 1); + break; + default: + assert(false); + } notifyListeners(); } From d57554e84e4d4d4fa619353f6209a1236e51628d Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 11:02:19 +0200 Subject: [PATCH 24/35] add minimal pdf implementation --- lib/model/export_import.dart | 5 +- lib/model/pdf_creator.dart | 42 ++++++++++ .../subsettings/export_import_screen.dart | 2 +- pubspec.lock | 80 +++++++++++++++++++ pubspec.yaml | 1 + 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 lib/model/pdf_creator.dart diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index d8d59de6..2aae36b4 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:blood_pressure_app/model/pdf_creator.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:csv/csv.dart'; @@ -12,7 +13,7 @@ class DataExporter { DataExporter(this.settings); - Uint8List createFile(List records) { + Future createFile(List records) async { if (settings.exportFormat == ExportFormat.csv) { List exportItems; if (settings.exportCustomEntries) { @@ -63,7 +64,7 @@ class DataExporter { var csvData = converter.convert(items); return Uint8List.fromList(utf8.encode(csvHead + csvData)); } else if (settings.exportFormat == ExportFormat.pdf) { - throw UnimplementedError('TODO'); + return await PdfCreator().createPdf(records); } return Uint8List(0); } diff --git a/lib/model/pdf_creator.dart b/lib/model/pdf_creator.dart new file mode 100644 index 00000000..df4725fd --- /dev/null +++ b/lib/model/pdf_creator.dart @@ -0,0 +1,42 @@ +import 'dart:typed_data'; + +import 'package:blood_pressure_app/model/blood_pressure.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; + +class PdfCreator { + Future createPdf(List data) async { + Document pdf = Document(); + + pdf.addPage(Page( + pageFormat: PdfPageFormat.a4, + build: (Context context) { + return Center( + child: Table( + children: [ + TableRow( + children: [ + Text('timestamp'), + Text('systolic'), + Text('diastolic'), + Text('pulse'), + Text('note') + ] + ), + for (var entry in data) + TableRow( + children: [ + Text(entry.creationTime.toIso8601String()), + Text(entry.systolic.toString()), + Text(entry.diastolic.toString()), + Text(entry.pulse.toString()), + Text(entry.notes) + ] + ) + ] + ), + ); + })); + return await pdf.save(); + } +} \ No newline at end of file diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index e60b1572..45707ad9 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -206,7 +206,7 @@ class _ExportImportScreenState extends State { } else { entries = await Provider.of(context, listen: false).all; } - var fileContents = DataExporter(settings).createFile(entries); + var fileContents = await DataExporter(settings).createFile(entries); String filename = 'blood_press_${DateTime.now().toIso8601String()}'; String path = await FileSaver.instance.saveFile(name: filename, bytes: fileContents); diff --git a/pubspec.lock b/pubspec.lock index 9e436f2d..d97c2bf8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + archive: + dependency: transitive + description: + name: archive + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + url: "https://pub.dev" + source: hosted + version: "3.3.7" args: dependency: transitive description: @@ -49,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "789f898eef0bd88312470bdb2cc996f895ad7dd5f89e9adde84b204546a90b45" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + bidi: + dependency: transitive + description: + name: bidi + sha256: dc00274c7edabae2ab30c676e736ea1eb0b1b7a1b436cb5fe372e431ccb39ab0 + url: "https://pub.dev" + source: hosted + version: "2.0.6" boolean_selector: dependency: transitive description: @@ -309,6 +333,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + url: "https://pub.dev" + source: hosted + version: "4.0.17" intl: dependency: "direct main" description: @@ -421,6 +453,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: transitive description: @@ -469,6 +509,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.6" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "9f75fc7f5580ea5e635b5724de58fb27f684c9ad03ed46fdc1aac768e4557315" + url: "https://pub.dev" + source: hosted + version: "3.10.4" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" platform: dependency: transitive description: @@ -485,6 +541,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" process: dependency: transitive description: @@ -509,6 +573,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" resource_portable: dependency: transitive description: @@ -810,6 +882,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c4c6a3e..10b36891 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: url_launcher: ^6.1.11 # BSD-3-Clause shared_preferences: ^2.1.1 # BSD-3-Clause mockito: ^5.4.1 + pdf: ^3.10.4 dev_dependencies: flutter_test: From 0c20a72dc1753d4d2f55d58a6603e8f114c1e7db Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 16:02:14 +0200 Subject: [PATCH 25/35] add file extensions --- lib/screens/subsettings/export_import_screen.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 45707ad9..4b22589c 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -208,7 +208,14 @@ class _ExportImportScreenState extends State { } var fileContents = await DataExporter(settings).createFile(entries); - String filename = 'blood_press_${DateTime.now().toIso8601String()}'; + String filename = 'blood_press_${DateTime.now().toIso8601String()}' + switch(settings.exportFormat) { + case ExportFormat.csv: + filename += '.csv'; + break; + case ExportFormat.pdf: + filename += '.pdf'; + } String path = await FileSaver.instance.saveFile(name: filename, bytes: fileContents); if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) && context.mounted) { From 9b5d3b041bb6c65f9a55e9dcad0ff4975dddcd21 Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 16:23:38 +0200 Subject: [PATCH 26/35] export as db file --- lib/l10n/app_en.arb | 1 + lib/model/export_import.dart | 114 ++++++++++-------- lib/model/settings_store.dart | 5 + .../subsettings/export_import_screen.dart | 13 +- 4 files changed, 82 insertions(+), 51 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5a12a23c..b9bcb93e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -94,6 +94,7 @@ "exportCsvHeadlineDesc": "Helps to discriminate types", "csv": "CSV", "pdf": "PDF", + "db": "SQLITE DB", "text": "text", "other": "other", "fieldDelimiter": "field delimiter", diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 2aae36b4..6e95c67a 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -1,10 +1,13 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:blood_pressure_app/model/pdf_creator.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:csv/csv.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; import 'blood_pressure.dart'; @@ -14,59 +17,65 @@ class DataExporter { DataExporter(this.settings); Future createFile(List records) async { - if (settings.exportFormat == ExportFormat.csv) { - List exportItems; - if (settings.exportCustomEntries) { - exportItems = settings.exportItems; - } else { - exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; - } + switch (settings.exportFormat) { + case ExportFormat.csv: + return createCSVCFile(records); + case ExportFormat.pdf: + return await PdfCreator().createPdf(records); + case ExportFormat.db: + return copyDBFile(); + } + } - var csvHead = ''; - if (settings.exportCsvHeadline) { - for (var i = 0; i records) { + List exportItems; + if (settings.exportCustomEntries) { + exportItems = settings.exportItems; + } else { + exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; + } + + var csvHead = ''; + if (settings.exportCsvHeadline) { + for (var i = 0; i> items = []; - for (var record in records) { - List row = []; - for (var attribute in exportItems) { - switch (attribute) { - case 'timestampUnixMs': - row.add(record.creationTime.millisecondsSinceEpoch); - break; - case 'isoUTCTime': - row.add(record.creationTime.toIso8601String()); - break; - case 'systolic': - row.add(record.systolic); - break; - case 'diastolic': - row.add(record.diastolic); - break; - case 'pulse': - row.add(record.pulse); - break; - case 'notes': - row.add(record.notes); - break; - } + List> items = []; + for (var record in records) { + List row = []; + for (var attribute in exportItems) { + switch (attribute) { + case 'timestampUnixMs': + row.add(record.creationTime.millisecondsSinceEpoch); + break; + case 'isoUTCTime': + row.add(record.creationTime.toIso8601String()); + break; + case 'systolic': + row.add(record.systolic); + break; + case 'diastolic': + row.add(record.diastolic); + break; + case 'pulse': + row.add(record.pulse); + break; + case 'notes': + row.add(record.notes); + break; } - items.add(row); } - var converter = ListToCsvConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter); - var csvData = converter.convert(items); - return Uint8List.fromList(utf8.encode(csvHead + csvData)); - } else if (settings.exportFormat == ExportFormat.pdf) { - return await PdfCreator().createPdf(records); + items.add(row); } - return Uint8List(0); + var converter = ListToCsvConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter); + var csvData = converter.convert(items); + return Uint8List.fromList(utf8.encode(csvHead + csvData)); } List parseCSVFile(Uint8List data) { @@ -126,9 +135,20 @@ class DataExporter { } return records; } + + Future copyDBFile() async { + var dbPath = await getDatabasesPath(); + + if (dbPath != inMemoryDatabasePath) { + dbPath = join(dbPath, 'blood_pressure.db'); + } + return File(dbPath).readAsBytes(); + + } } enum ExportFormat { csv, - pdf + pdf, + db } \ No newline at end of file diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 142abb5d..2938052b 100644 --- a/lib/model/settings_store.dart +++ b/lib/model/settings_store.dart @@ -279,6 +279,8 @@ class Settings extends ChangeNotifier { return ExportFormat.csv; case 1: return ExportFormat.pdf; + case 2: + return ExportFormat.db; default: assert(false); return ExportFormat.csv; @@ -293,6 +295,9 @@ class Settings extends ChangeNotifier { case ExportFormat.pdf: _prefs.setInt('exportFormat', 1); break; + case ExportFormat.db: + _prefs.setInt('exportFormat', 2); + break; default: assert(false); } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 4b22589c..f7369c13 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -75,6 +75,7 @@ class _ExportImportScreenState extends State { items: [ DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + DropdownMenuItem(value: ExportFormat.db, child: Text(AppLocalizations.of(context)!.db)), ], onChanged: (ExportFormat? value) { if (value != null) { @@ -146,8 +147,8 @@ class _ExportImportScreenState extends State { ]; } - // warn banner when - if (_showWarnBanner && (settings.exportFormat != ExportFormat.csv || + // warn banner when the exported data can't be imported + if (_showWarnBanner && ![ExportFormat.csv, ExportFormat.db].contains(settings.exportFormat) || settings.exportCsvHeadline == false || !( (settings.exportItems.contains('timestampUnixMs') || settings.exportItems.contains('isoUTCTime')) && @@ -156,7 +157,7 @@ class _ExportImportScreenState extends State { settings.exportItems.contains('pulse') && settings.exportItems.contains('notes') ) - )) { + ) { options.insert(0, MaterialBanner( padding: const EdgeInsets.all(20), content: Text(AppLocalizations.of(context)!.exportWarnConfigNotImportable), @@ -208,13 +209,17 @@ class _ExportImportScreenState extends State { } var fileContents = await DataExporter(settings).createFile(entries); - String filename = 'blood_press_${DateTime.now().toIso8601String()}' + String filename = 'blood_press_${DateTime.now().toIso8601String()}'; switch(settings.exportFormat) { case ExportFormat.csv: filename += '.csv'; break; case ExportFormat.pdf: filename += '.pdf'; + break; + case ExportFormat.db: + filename += '.db'; + break; } String path = await FileSaver.instance.saveFile(name: filename, bytes: fileContents); From ba5672ea3239d71f08c7a3b8ce5b1530d404d399 Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 16:26:32 +0200 Subject: [PATCH 27/35] move pdf creation to export file --- lib/model/export_import.dart | 40 ++++++++++++++++++++++++++++++++-- lib/model/pdf_creator.dart | 42 ------------------------------------ 2 files changed, 38 insertions(+), 44 deletions(-) delete mode 100644 lib/model/pdf_creator.dart diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 6e95c67a..ecfa8d25 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -3,10 +3,11 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:blood_pressure_app/model/pdf_creator.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:csv/csv.dart'; import 'package:path/path.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; import 'package:sqflite/sqflite.dart'; import 'blood_pressure.dart'; @@ -21,7 +22,7 @@ class DataExporter { case ExportFormat.csv: return createCSVCFile(records); case ExportFormat.pdf: - return await PdfCreator().createPdf(records); + return createPdfFile(records); case ExportFormat.db: return copyDBFile(); } @@ -136,6 +137,41 @@ class DataExporter { return records; } + Future createPdfFile(List data) async { + pw.Document pdf = pw.Document(); + + pdf.addPage(pw.Page( + pageFormat: PdfPageFormat.a4, + build: (pw.Context context) { + return pw.Center( + child: pw.Table( + children: [ + pw.TableRow( + children: [ + pw.Text('timestamp'), + pw.Text('systolic'), + pw.Text('diastolic'), + pw.Text('pulse'), + pw.Text('note') + ] + ), + for (var entry in data) + pw.TableRow( + children: [ + pw.Text(entry.creationTime.toIso8601String()), + pw.Text(entry.systolic.toString()), + pw.Text(entry.diastolic.toString()), + pw.Text(entry.pulse.toString()), + pw.Text(entry.notes) + ] + ) + ] + ), + ); + })); + return await pdf.save(); + } + Future copyDBFile() async { var dbPath = await getDatabasesPath(); diff --git a/lib/model/pdf_creator.dart b/lib/model/pdf_creator.dart deleted file mode 100644 index df4725fd..00000000 --- a/lib/model/pdf_creator.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:typed_data'; - -import 'package:blood_pressure_app/model/blood_pressure.dart'; -import 'package:pdf/pdf.dart'; -import 'package:pdf/widgets.dart'; - -class PdfCreator { - Future createPdf(List data) async { - Document pdf = Document(); - - pdf.addPage(Page( - pageFormat: PdfPageFormat.a4, - build: (Context context) { - return Center( - child: Table( - children: [ - TableRow( - children: [ - Text('timestamp'), - Text('systolic'), - Text('diastolic'), - Text('pulse'), - Text('note') - ] - ), - for (var entry in data) - TableRow( - children: [ - Text(entry.creationTime.toIso8601String()), - Text(entry.systolic.toString()), - Text(entry.diastolic.toString()), - Text(entry.pulse.toString()), - Text(entry.notes) - ] - ) - ] - ), - ); - })); - return await pdf.save(); - } -} \ No newline at end of file From 57c41b46aa091dc04c0be536f46c257cbeeef3dd Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 16:33:08 +0200 Subject: [PATCH 28/35] more expandable structure for DataExporter class --- lib/model/export_import.dart | 15 +++++++++++++++ .../subsettings/export_import_screen.dart | 18 +++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index ecfa8d25..062b5c4c 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -28,6 +28,21 @@ class DataExporter { } } + List? parseFile(Uint8List data) { + switch(settings.exportFormat) { + case ExportFormat.csv: + try { + return parseCSVFile(data); + } catch (e) { + return null; + } + case ExportFormat.pdf: + return null; + case ExportFormat.db: + throw UnimplementedError('TODO'); + } + } + Uint8List createCSVCFile(List records) { List exportItems; if (settings.exportCustomEntries) { diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index f7369c13..954777d6 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -274,17 +274,17 @@ class _ExportImportScreenState extends State { return; } - try { - var fileContents = DataExporter(settings).parseCSVFile(binaryContent); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length)))); - var model = Provider.of(context, listen: false); - for (final e in fileContents) { - model.add(e); - } - } catch (e) { + var fileContents = DataExporter(settings).parseFile(binaryContent); + if (fileContents == null) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.errNotImportable))); + return; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length)))); + var model = Provider.of(context, listen: false); + for (final e in fileContents) { + model.add(e); } }, ) From 70864b4e563353b245449c09d199cf2f916ae468 Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 17:01:33 +0200 Subject: [PATCH 29/35] refactor ExportImportScreen to be more manageable --- .../subsettings/export_import_screen.dart | 537 +++++++++--------- 1 file changed, 279 insertions(+), 258 deletions(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index 954777d6..d314ec80 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -14,16 +14,9 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; -class ExportImportScreen extends StatefulWidget { +class ExportImportScreen extends StatelessWidget { const ExportImportScreen({super.key}); - @override - State createState() => _ExportImportScreenState(); -} - -class _ExportImportScreenState extends State { - bool _showWarnBanner = true; - @override Widget build(BuildContext context) { return Scaffold( @@ -34,266 +27,134 @@ class _ExportImportScreenState extends State { body: Container( margin: const EdgeInsets.only(bottom: 80), child: Consumer(builder: (context, settings, child) { - // export range - var exportRange = settings.exportDataRange; - String exportRangeText; - if (exportRange.start.millisecondsSinceEpoch != 0 && exportRange.end.millisecondsSinceEpoch != 0) { - var formatter = DateFormat.yMMMd(AppLocalizations.of(context)!.localeName); - exportRangeText = '${formatter.format(exportRange.start)} - ${formatter.format(exportRange.end)}'; - } else { - exportRangeText = AppLocalizations.of(context)!.errPleaseSelect; - } - - // default options - List options = [ - SwitchSettingsTile( - title: Text(AppLocalizations.of(context)!.exportLimitDataRange), - initialValue: settings.exportLimitDataRange, - onToggle: (value) { - settings.exportLimitDataRange = value; - } - ), - (settings.exportLimitDataRange) ? SettingsTile( - title: Text(AppLocalizations.of(context)!.exportInterval), - description: Text(exportRangeText), - onPressed: (context) async { - var model = Provider.of(context, listen: false); - var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); - if (newRange == null && context.mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); - return; - } - settings.exportDataRange = newRange ?? DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(0), end: DateTime.fromMillisecondsSinceEpoch(0)); - - } - ) : const SizedBox.shrink(), - DropDownSettingsTile( - key: const Key('exportFormat'), - title: Text(AppLocalizations.of(context)!.exportFormat), - value: settings.exportFormat, - items: [ - DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), - DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), - DropdownMenuItem(value: ExportFormat.db, child: Text(AppLocalizations.of(context)!.db)), - ], - onChanged: (ExportFormat? value) { - if (value != null) { - settings.exportFormat = value; - } - }, - ), - /* - DropDownSettingsTile( - key: const Key('exportMimeType'), - title: Text(AppLocalizations.of(context)!.exportMimeType), - description: Text(AppLocalizations.of(context)!.exportMimeTypeDesc), - value: settings.exportMimeType, - items: [ - DropdownMenuItem(value: MimeType.csv, child: Text(AppLocalizations.of(context)!.csv)), - DropdownMenuItem(value: MimeType.text, child: Text(AppLocalizations.of(context)!.text)), - DropdownMenuItem(value: MimeType.pdf, child: Text(AppLocalizations.of(context)!.pdf)), - DropdownMenuItem(value: MimeType.other, child: Text(AppLocalizations.of(context)!.other)), + return SingleChildScrollView( + child: Column( + children: [ + const ExportWarnBanner(), + DropDownSettingsTile( + key: const Key('exportFormat'), + title: Text(AppLocalizations.of(context)!.exportFormat), + value: settings.exportFormat, + items: [ + DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), + DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + DropdownMenuItem(value: ExportFormat.db, child: Text(AppLocalizations.of(context)!.db)), + ], + onChanged: (ExportFormat? value) { + if (value != null) { + settings.exportFormat = value; + } + }, + ), + const ExportDataRangeSettings(), + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.fieldDelimiter), + inputWidth: 40, + initialValue: settings.csvFieldDelimiter, + disabled: !(settings.exportFormat == ExportFormat.csv), + onEditingComplete: (value) { + if (value != null) { + settings.csvFieldDelimiter = value; + } + }, + ), + InputSettingsTile( + title: Text(AppLocalizations.of(context)!.textDelimiter), + inputWidth: 40, + initialValue: settings.csvTextDelimiter, + disabled: !(settings.exportFormat == ExportFormat.csv), + onEditingComplete: (value) { + if (value != null) { + settings.csvTextDelimiter = value; + } + }, + ), + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportCsvHeadline), + description: Text(AppLocalizations.of(context)!.exportCsvHeadlineDesc), + initialValue: settings.exportCsvHeadline, + disabled: !(settings.exportFormat == ExportFormat.csv), + onToggle: (value) { + settings.exportCsvHeadline = value; + } + ), + const ExportFieldCustomisationSetting(), ], - onChanged: (MimeType? value) { - if (value != null) { - settings.exportMimeType = value; - } - }, ), - */ - ]; - - // mode specifics - List modeSpecificSettings = []; - if (settings.exportFormat == ExportFormat.csv) { - modeSpecificSettings = [ - InputSettingsTile( - title: Text(AppLocalizations.of(context)!.fieldDelimiter), - inputWidth: 40, - initialValue: settings.csvFieldDelimiter, - onEditingComplete: (value) { - if (value != null) { - settings.csvFieldDelimiter = value; - } - }, - ), - InputSettingsTile( - title: Text(AppLocalizations.of(context)!.textDelimiter), - inputWidth: 40, - initialValue: settings.csvTextDelimiter, - onEditingComplete: (value) { - if (value != null) { - settings.csvTextDelimiter = value; - } - }, - ), - SwitchSettingsTile( - title: Text(AppLocalizations.of(context)!.exportCsvHeadline), - description: Text(AppLocalizations.of(context)!.exportCsvHeadlineDesc), - initialValue: settings.exportCsvHeadline, - onToggle: (value) { - settings.exportCsvHeadline = value; - } - ), - SwitchSettingsTile( - title: Text(AppLocalizations.of(context)!.exportCustomEntries), - initialValue: settings.exportCustomEntries, - onToggle: (value) { - settings.exportCustomEntries = value; - } - ), - (settings.exportCustomEntries) ? const CsvItemsOrderCreator(): const SizedBox.shrink() - ]; - } - - // warn banner when the exported data can't be imported - if (_showWarnBanner && ![ExportFormat.csv, ExportFormat.db].contains(settings.exportFormat) || - settings.exportCsvHeadline == false || - !( - (settings.exportItems.contains('timestampUnixMs') || settings.exportItems.contains('isoUTCTime')) && - settings.exportItems.contains('systolic') && - settings.exportItems.contains('diastolic') && - settings.exportItems.contains('pulse') && - settings.exportItems.contains('notes') - ) - ) { - options.insert(0, MaterialBanner( - padding: const EdgeInsets.all(20), - content: Text(AppLocalizations.of(context)!.exportWarnConfigNotImportable), - actions: [ - TextButton( - onPressed: () { - setState(() { - _showWarnBanner = false; - }); - }, - child: Text(AppLocalizations.of(context)!.btnConfirm)) - ] - )); - } - - // create view - options.addAll(modeSpecificSettings); - options.add(const SizedBox(height: 20,)); - return ListView( - children: options, ); - }), + }) ), - floatingActionButton: Container( - height: 60, - color: Theme.of(context).colorScheme.onBackground, - child: Center( - child: Row( - children: [ - Expanded( - flex: 50, - child: MaterialButton( - height: 60, - child: Text(AppLocalizations.of(context)!.export), - onPressed: () async { - var settings = Provider.of(context, listen: false); + floatingActionButton: const ExportImportButtons(), + ); + } +} - final UnmodifiableListView entries; - if (settings.exportLimitDataRange) { - var range = settings.exportDataRange; - if (range.start.millisecondsSinceEpoch == 0 || range.end.millisecondsSinceEpoch == 0) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); - return; - } - entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange.start, settings.exportDataRange.end); - } else { - entries = await Provider.of(context, listen: false).all; - } - var fileContents = await DataExporter(settings).createFile(entries); +class ExportDataRangeSettings extends StatelessWidget { + const ExportDataRangeSettings({super.key}); - String filename = 'blood_press_${DateTime.now().toIso8601String()}'; - switch(settings.exportFormat) { - case ExportFormat.csv: - filename += '.csv'; - break; - case ExportFormat.pdf: - filename += '.pdf'; - break; - case ExportFormat.db: - filename += '.db'; - break; - } - String path = await FileSaver.instance.saveFile(name: filename, bytes: fileContents); + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, settings, child) { + var exportRange = settings.exportDataRange; + String exportRangeText; + if (exportRange.start.millisecondsSinceEpoch != 0 && exportRange.end.millisecondsSinceEpoch != 0) { + var formatter = DateFormat.yMMMd(AppLocalizations.of(context)!.localeName); + exportRangeText = '${formatter.format(exportRange.start)} - ${formatter.format(exportRange.end)}'; + } else { + exportRangeText = AppLocalizations.of(context)!.errPleaseSelect; + } + return Column( + children: [ + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportLimitDataRange), + initialValue: settings.exportLimitDataRange, + onToggle: (value) { + settings.exportLimitDataRange = value; + }, + disabled: settings.exportFormat == ExportFormat.db, + ), + SettingsTile( + title: Text(AppLocalizations.of(context)!.exportInterval), + description: Text(exportRangeText), + disabled: !settings.exportLimitDataRange, + onPressed: (context) async { + var model = Provider.of(context, listen: false); + var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); + if (newRange == null && context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); + return; + } + settings.exportDataRange = newRange ?? DateTimeRange(start: DateTime.fromMillisecondsSinceEpoch(0), end: DateTime.fromMillisecondsSinceEpoch(0)); + } + ), + ], + ); + }); + } - if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) && context.mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.success(path)))); - } else if (Platform.isAndroid || Platform.isIOS) { - Share.shareXFiles([ - XFile( - path, - mimeType: MimeType.csv.type - ) - ]); - } else { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('UNSUPPORTED PLATFORM'))); - } - }, - ) - ), - const VerticalDivider(), - Expanded( - flex: 50, - child: MaterialButton( - height: 60, - child: Text(AppLocalizations.of(context)!.import), - onPressed: () async { - final settings = Provider.of(context, listen: false); - if (!(settings.exportFormat == ExportFormat.csv)) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.errNotCsvFormat))); - } - if (!settings.exportCsvHeadline) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.errNeedHeadline))); - } +} - var result = await FilePicker.platform.pickFiles( - allowMultiple: false, - withData: true, - ); - if (!context.mounted) return; - if (result == null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.errNoFileOpened))); - return; - } - var binaryContent = result.files.single.bytes; - if (binaryContent == null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.errCantReadFile))); - return; - } +class ExportFieldCustomisationSetting extends StatelessWidget { + const ExportFieldCustomisationSetting({super.key}); - var fileContents = DataExporter(settings).parseFile(binaryContent); - if (fileContents == null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.errNotImportable))); - return; - } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length)))); - var model = Provider.of(context, listen: false); - for (final e in fileContents) { - model.add(e); - } - }, - ) - ), - ], + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, settings, child) { + return Column( + children: [ + SwitchSettingsTile( + title: Text(AppLocalizations.of(context)!.exportCustomEntries), + initialValue: settings.exportCustomEntries, + disabled: settings.exportFormat != ExportFormat.csv, + onToggle: (value) { + settings.exportCustomEntries = value; + } ), - ), - ), - ); + (settings.exportFormat == ExportFormat.csv && settings.exportCustomEntries) ? const CsvItemsOrderCreator() : const SizedBox.shrink() + ], + ); + }); } } @@ -398,6 +259,166 @@ class CsvItemsOrderCreator extends StatelessWidget { ); }); } +} +class ExportImportButtons extends StatelessWidget { + const ExportImportButtons({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 60, + color: Theme.of(context).colorScheme.onBackground, + child: Center( + child: Row( + children: [ + Expanded( + flex: 50, + child: MaterialButton( + height: 60, + child: Text(AppLocalizations.of(context)!.export), + onPressed: () async { + var settings = Provider.of(context, listen: false); + + final UnmodifiableListView entries; + if (settings.exportLimitDataRange) { + var range = settings.exportDataRange; + if (range.start.millisecondsSinceEpoch == 0 || range.end.millisecondsSinceEpoch == 0) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errNoRangeForExport))); + return; + } + entries = await Provider.of(context, listen: false).getInTimeRange(settings.exportDataRange.start, settings.exportDataRange.end); + } else { + entries = await Provider.of(context, listen: false).all; + } + var fileContents = await DataExporter(settings).createFile(entries); + + String filename = 'blood_press_${DateTime.now().toIso8601String()}'; + switch(settings.exportFormat) { + case ExportFormat.csv: + filename += '.csv'; + break; + case ExportFormat.pdf: + filename += '.pdf'; + break; + case ExportFormat.db: + filename += '.db'; + break; + } + String path = await FileSaver.instance.saveFile(name: filename, bytes: fileContents); + + if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) && context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.success(path)))); + } else if (Platform.isAndroid || Platform.isIOS) { + Share.shareXFiles([ + XFile( + path, + mimeType: MimeType.csv.type + ) + ]); + } else { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('UNSUPPORTED PLATFORM'))); + } + }, + ) + ), + const VerticalDivider(), + Expanded( + flex: 50, + child: MaterialButton( + height: 60, + child: Text(AppLocalizations.of(context)!.import), + onPressed: () async { + final settings = Provider.of(context, listen: false); + if (!(settings.exportFormat == ExportFormat.csv)) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNotCsvFormat))); + } + if (!settings.exportCsvHeadline) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNeedHeadline))); + } + + var result = await FilePicker.platform.pickFiles( + allowMultiple: false, + withData: true, + ); + if (!context.mounted) return; + if (result == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNoFileOpened))); + return; + } + var binaryContent = result.files.single.bytes; + if (binaryContent == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errCantReadFile))); + return; + } + + var fileContents = DataExporter(settings).parseFile(binaryContent); + if (fileContents == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.errNotImportable))); + return; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length)))); + var model = Provider.of(context, listen: false); + for (final e in fileContents) { + model.add(e); + } + }, + ) + ), + ], + ), + ), + ); + } +} + +class ExportWarnBanner extends StatefulWidget { + const ExportWarnBanner({super.key}); + + @override + State createState() => _ExportWarnBannerState(); } +class _ExportWarnBannerState extends State { + bool _showWarnBanner = true; + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, settings, child) { + if (_showWarnBanner && ![ExportFormat.csv, ExportFormat.db].contains(settings.exportFormat) || + settings.exportCsvHeadline == false || + !( + (settings.exportItems.contains('timestampUnixMs') || settings.exportItems.contains('isoUTCTime')) && + settings.exportItems.contains('systolic') && + settings.exportItems.contains('diastolic') && + settings.exportItems.contains('pulse') && + settings.exportItems.contains('notes') + ) + ) { + return MaterialBanner( + padding: const EdgeInsets.all(20), + content: Text(AppLocalizations.of(context)!.exportWarnConfigNotImportable), + actions: [ + TextButton( + onPressed: () { + setState(() { + _showWarnBanner = false; + }); + }, + child: Text(AppLocalizations.of(context)!.btnConfirm)) + ] + ); + } + return const SizedBox.shrink(); + }); + } + } + From 787d13dce311565600d6364e794f9445038e5b00 Mon Sep 17 00:00:00 2001 From: derdilla Date: Thu, 22 Jun 2023 17:09:02 +0200 Subject: [PATCH 30/35] allow sqlite import --- lib/l10n/app_de.arb | 2 +- lib/l10n/app_en.arb | 2 +- lib/screens/subsettings/export_import_screen.dart | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index a63e1f5f..2a42bbed 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -37,7 +37,7 @@ "errNoData": "Keine Daten", "errNoRangeForExport": "Sie müssen angeben, welche daten sie exportieren wollen.", "errPleaseSelect": "Bitte auswählen", - "errNotCsvFormat": "Es können nur Dateien im csv Format importiert werden.", + "errWrongImportFormat": "Es können nur Dateien im csv und SQLite db Format importiert werden.", "errNeedHeadline": "Es können nur Dateien mit einer Überschrift importiert werden.", "errNotImportable": "Diese Datei kann nicht importiert werden.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b9bcb93e..7191de57 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -37,7 +37,7 @@ "errNoData": "no data", "errNoRangeForExport": "You need to specify a range in which data is exported.", "errPleaseSelect": "please select", - "errNotCsvFormat": "You can only import files in csv format.", + "errWrongImportFormat": "You can only import files in csv and SQLite db format.", "errNeedHeadline": "You can only import files with a headline.", "errCantReadFile": "The file contents can not be read", "errNotImportable": "This file can't be imported", diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index d314ec80..d61bf96f 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -333,13 +333,15 @@ class ExportImportButtons extends StatelessWidget { child: Text(AppLocalizations.of(context)!.import), onPressed: () async { final settings = Provider.of(context, listen: false); - if (!(settings.exportFormat == ExportFormat.csv)) { + if (!([ExportFormat.csv, ExportFormat.db].contains(settings.exportFormat))) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.errNotCsvFormat))); + content: Text(AppLocalizations.of(context)!.errWrongImportFormat))); + return; } - if (!settings.exportCsvHeadline) { + if (settings.exportFormat == ExportFormat.csv && !settings.exportCsvHeadline) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.errNeedHeadline))); + return; } var result = await FilePicker.platform.pickFiles( From c59b0452e0256d2b1109c7536619ef9eb55778ab Mon Sep 17 00:00:00 2001 From: derdilla Date: Fri, 23 Jun 2023 09:23:48 +0200 Subject: [PATCH 31/35] add DB parse --- lib/model/blood_pressure.dart | 8 ++++---- lib/model/export_import.dart | 8 ++++++-- lib/screens/subsettings/export_import_screen.dart | 5 ++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/model/blood_pressure.dart b/lib/model/blood_pressure.dart index 5f8aa3ac..f4d08577 100644 --- a/lib/model/blood_pressure.dart +++ b/lib/model/blood_pressure.dart @@ -10,10 +10,10 @@ class BloodPressureModel extends ChangeNotifier { late final Database _database; BloodPressureModel._create(); - Future _asyncInit(String? dbPath) async { + Future _asyncInit(String? dbPath, bool isFullPath) async { dbPath ??= await getDatabasesPath(); - if (dbPath != inMemoryDatabasePath) { + if (dbPath != inMemoryDatabasePath && !isFullPath) { dbPath = join(dbPath, 'blood_pressure.db'); } @@ -29,7 +29,7 @@ class BloodPressureModel extends ChangeNotifier { } // factory method, to allow for async constructor - static Future create({String? dbPath}) async { + static Future create({String? dbPath, bool isFullPath = false}) async { if (Platform.isWindows || Platform.isLinux) { // Initialize FFI sqfliteFfiInit(); @@ -38,7 +38,7 @@ class BloodPressureModel extends ChangeNotifier { } final component = BloodPressureModel._create(); - await component._asyncInit(dbPath); + await component._asyncInit(dbPath, isFullPath); return component; } diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index 062b5c4c..b24d9586 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -28,7 +28,7 @@ class DataExporter { } } - List? parseFile(Uint8List data) { + Future?> parseFile(String filePath, Uint8List data) async { switch(settings.exportFormat) { case ExportFormat.csv: try { @@ -39,7 +39,7 @@ class DataExporter { case ExportFormat.pdf: return null; case ExportFormat.db: - throw UnimplementedError('TODO'); + return loadDBFile(filePath); } } @@ -194,7 +194,11 @@ class DataExporter { dbPath = join(dbPath, 'blood_pressure.db'); } return File(dbPath).readAsBytes(); + } + Future> loadDBFile(String filePath) async { + final loadedModel = await BloodPressureModel.create(dbPath: filePath, isFullPath: true); + return await loadedModel.all; } } diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index d61bf96f..f50efc91 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -360,8 +360,11 @@ class ExportImportButtons extends StatelessWidget { content: Text(AppLocalizations.of(context)!.errCantReadFile))); return; } + var path = result.files.single.path; + assert(path != null); // null state directly linked to binary content - var fileContents = DataExporter(settings).parseFile(binaryContent); + var fileContents = await DataExporter(settings).parseFile(path! ,binaryContent); + if (!context.mounted) return; if (fileContents == null) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.errNotImportable))); From d87d693824c9920f5142bb879f495d93ff223a92 Mon Sep 17 00:00:00 2001 From: derdilla Date: Fri, 23 Jun 2023 09:25:24 +0200 Subject: [PATCH 32/35] fix invalid option showing --- lib/screens/subsettings/export_import_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index f50efc91..ae791ed4 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -116,7 +116,7 @@ class ExportDataRangeSettings extends StatelessWidget { SettingsTile( title: Text(AppLocalizations.of(context)!.exportInterval), description: Text(exportRangeText), - disabled: !settings.exportLimitDataRange, + disabled: !settings.exportLimitDataRange || settings.exportFormat == ExportFormat.db, onPressed: (context) async { var model = Provider.of(context, listen: false); var newRange = await showDateRangePicker(context: context, firstDate: await model.firstDay, lastDate: await model.lastDay); From 846413e9545bed833dda8b73eb3fd60074705a6d Mon Sep 17 00:00:00 2001 From: derdilla Date: Fri, 23 Jun 2023 09:29:20 +0200 Subject: [PATCH 33/35] fix localization --- lib/l10n/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7191de57..c8bcc212 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -87,7 +87,7 @@ "exportInterval": "data range", "exportFormat": "export format", "exportCustomEntries": "customize fields", - "addEntry": "Feld hinzufügen", + "addEntry": "Add field", "exportMimeType": "export MIME type", "exportMimeTypeDesc": "signalizes type to other apps", "exportCsvHeadline": "headline", From 9dd90cbc401f44499eadc246f625a7b73fde19d9 Mon Sep 17 00:00:00 2001 From: derdilla Date: Fri, 23 Jun 2023 09:32:43 +0200 Subject: [PATCH 34/35] hide pdf option and fix file extension to finalize --- lib/screens/subsettings/export_import_screen.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index ae791ed4..2f945f0f 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -37,7 +37,7 @@ class ExportImportScreen extends StatelessWidget { value: settings.exportFormat, items: [ DropdownMenuItem(value: ExportFormat.csv, child: Text(AppLocalizations.of(context)!.csv)), - DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), + //DropdownMenuItem(value: ExportFormat.pdf, child: Text(AppLocalizations.of(context)!.pdf)), DropdownMenuItem(value: ExportFormat.db, child: Text(AppLocalizations.of(context)!.db)), ], onChanged: (ExportFormat? value) { @@ -295,18 +295,19 @@ class ExportImportButtons extends StatelessWidget { var fileContents = await DataExporter(settings).createFile(entries); String filename = 'blood_press_${DateTime.now().toIso8601String()}'; + String ext; switch(settings.exportFormat) { case ExportFormat.csv: - filename += '.csv'; + ext = '.csv'; break; case ExportFormat.pdf: - filename += '.pdf'; + ext = '.pdf'; break; case ExportFormat.db: - filename += '.db'; + ext = '.db'; break; } - String path = await FileSaver.instance.saveFile(name: filename, bytes: fileContents); + String path = await FileSaver.instance.saveFile(name: filename, ext: ext, bytes: fileContents); if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) && context.mounted) { ScaffoldMessenger.of(context) From 1e8c38508ef65c9b6a7f252e0384386f021ced83 Mon Sep 17 00:00:00 2001 From: derdilla Date: Fri, 23 Jun 2023 09:44:02 +0200 Subject: [PATCH 35/35] add missing localizations --- .gitignore | 1 + l10n.yaml | 3 ++- lib/l10n/app_de.arb | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1e278a11..38b3aad5 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ app.*.map.json /android/app/profile /android/app/release /main.dart.incremental.dill +/l10n_errors.txt diff --git a/l10n.yaml b/l10n.yaml index 4e6692e5..afa2d41b 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,4 @@ arb-dir: lib/l10n template-arb-file: app_en.arb -output-localization-file: app_localizations.dart \ No newline at end of file +output-localization-file: app_localizations.dart +untranslated-messages-file: l10n_errors.txt \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2a42bbed..4c187508 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -39,6 +39,7 @@ "errPleaseSelect": "Bitte auswählen", "errWrongImportFormat": "Es können nur Dateien im csv und SQLite db Format importiert werden.", "errNeedHeadline": "Es können nur Dateien mit einer Überschrift importiert werden.", + "errCantReadFile": "Der Inhalt der Datei kann nicht gelesen werden", "errNotImportable": "Diese Datei kann nicht importiert werden.", @@ -94,6 +95,7 @@ "exportCsvHeadlineDesc": "Feldbezeichnungen zum Differenzieren", "csv": "CSV", "pdf": "PDF", + "db": "SQLITE DB", "text": "Text", "other": "Anderes", "fieldDelimiter": "Feldseparator", @@ -113,6 +115,14 @@ "import": "IMPORT", "sourceCode": "Quellcode", "licenses": "Lizenzen dritter", + "importSuccess": "Es wurden {count} Einträge erfolgreich importiert", + "@importSuccess": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "statistics": "Statistik", "measurementCount": "Anzahl Messungen",