Commit 5d1639a

derdilla <derdilla06@gmail.com>
2023-06-26 04:37:38
add option to export after every entry
1 parent e0c74fa
lib/components/measurement_list.dart
@@ -93,10 +93,9 @@ class MeasurementList extends StatelessWidget {
                                   Dismissible(
                                     key: Key(data[index].creationTime.toIso8601String()),
                                     confirmDismiss: (direction) async {
-                                      if (direction == DismissDirection.startToEnd) {
-                                        // edit
-                                        Provider.of<BloodPressureModel>(context, listen: false)
-                                            .delete(data[index].creationTime);
+                                      final model = Provider.of<BloodPressureModel>(context, listen: false);
+                                      if (direction == DismissDirection.startToEnd) { // edit
+                                        model.delete(data[index].creationTime);
                                         Navigator.push(
                                           context,
                                           MaterialPageRoute(
@@ -110,8 +109,7 @@ class MeasurementList extends StatelessWidget {
                                                   )),
                                         );
                                         return false;
-                                      } else {
-                                        // delete
+                                      } else { // delete
                                         bool dialogeDeletionConfirmed = false;
                                         if (settings.confirmDeletion) {
                                           await showDialog(
@@ -126,8 +124,8 @@ class MeasurementList extends StatelessWidget {
                                                         child: Text(AppLocalizations.of(context)!.btnCancel)),
                                                     ElevatedButton(
                                                         onPressed: () {
-                                                          Provider.of<BloodPressureModel>(context, listen: false)
-                                                              .delete(data[index].creationTime);
+                                                          model.delete(data[index].creationTime);
+
                                                           dialogeDeletionConfirmed = true;
                                                           Navigator.of(context).pop();
                                                         },
@@ -136,8 +134,7 @@ class MeasurementList extends StatelessWidget {
                                                 );
                                               });
                                         } else {
-                                          Provider.of<BloodPressureModel>(context, listen: false)
-                                              .delete(data[index].creationTime);
+                                          model.delete(data[index].creationTime);
                                           dialogeDeletionConfirmed = true;
                                         }
 
@@ -149,12 +146,14 @@ class MeasurementList extends StatelessWidget {
                                             content: Text(AppLocalizations.of(context)!.deletionConfirmed),
                                             action: SnackBarAction(
                                               label: AppLocalizations.of(context)!.btnUndo,
-                                              onPressed: () => model.add(BloodPressureRecord(
-                                                  data[index].creationTime,
-                                                  data[index].systolic,
-                                                  data[index].diastolic,
-                                                  data[index].pulse,
-                                                  data[index].notes)),
+                                              onPressed: () async {
+                                                model.add(BloodPressureRecord(
+                                                    data[index].creationTime,
+                                                    data[index].systolic,
+                                                    data[index].diastolic,
+                                                    data[index].pulse,
+                                                    data[index].notes));
+                                              },
                                             ),
                                           ));
                                         }
lib/l10n/app_de.arb
@@ -85,7 +85,9 @@
   "data": "Daten",
 
   "exportImport": "Exportieren / Importieren",
-  "exportDir": "Export ordbner",
+  "exportDir": "Export ordner",
+  "exportAfterEveryInput": "Export nach jedem Eintrag",
+  "exportAfterEveryInputDesc": "Nicht empfohlen (datenexplosion)",
   "exportLimitDataRange": "Datenbereich einschränken",
   "exportInterval": "Datenbereich",
   "exportFormat": "Exportformat",
lib/l10n/app_en.arb
@@ -85,6 +85,8 @@
 
   "exportImport": "Export / Import",
   "exportDir": "Export directory",
+  "exportAfterEveryInput": "Export after every entry",
+  "exportAfterEveryInputDesc": "Not recommended (file explosion)",
   "exportLimitDataRange": "Limit data range",
   "exportInterval": "Data range",
   "exportFormat": "Export format",
lib/model/export_import.dart
@@ -1,21 +1,29 @@
 
+import 'dart:collection';
 import 'dart:convert';
 import 'dart:io';
 import 'dart:typed_data';
 
 import 'package:blood_pressure_app/model/settings_store.dart';
 import 'package:csv/csv.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';
+import 'package:jsaver/jSaver.dart';
 import 'package:path/path.dart';
 import 'package:pdf/pdf.dart';
 import 'package:pdf/widgets.dart' as pw;
+import 'package:provider/provider.dart';
+import 'package:share_plus/share_plus.dart';
 import 'package:sqflite/sqflite.dart';
 
 import 'blood_pressure.dart';
 
-class DataExporter {
+class ExportFileCreator {
   Settings settings;
 
-  DataExporter(this.settings);
+  ExportFileCreator(this.settings);
 
   Future<Uint8List> createFile(List<BloodPressureRecord> records) async {
     switch (settings.exportFormat) {
@@ -202,6 +210,109 @@ class DataExporter {
   }
 }
 
+class Exporter {
+  BuildContext context;
+  Exporter(this.context);
+
+  Future<void> export() async {
+    var settings = Provider.of<Settings>(context, listen: false);
+    final messenger = ScaffoldMessenger.of(context);
+    final localizations = AppLocalizations.of(context);
+
+    final UnmodifiableListView<BloodPressureRecord> entries;
+    if (settings.exportLimitDataRange) {
+      var range = settings.exportDataRange;
+      if (range.start.millisecondsSinceEpoch == 0 || range.end.millisecondsSinceEpoch == 0) {
+        messenger.showSnackBar(SnackBar(content: Text(localizations!.errNoRangeForExport)));
+        return;
+      }
+      entries = await Provider.of<BloodPressureModel>(context, listen: false).getInTimeRange(settings.exportDataRange.start, settings.exportDataRange.end);
+    } else {
+      entries = await Provider.of<BloodPressureModel>(context, listen: false).all;
+    }
+    var fileContents = await ExportFileCreator(settings).createFile(entries);
+
+    String filename = 'blood_press_${DateTime.now().toIso8601String()}';
+    String ext;
+    switch(settings.exportFormat) {
+      case ExportFormat.csv:
+        ext = 'CSV'; // lower case 'csv' gets automatically converted to 'csv.xls' for some reason
+        break;
+      case ExportFormat.pdf:
+        ext = 'pdf';
+        break;
+      case ExportFormat.db:
+        ext = 'db';
+        break;
+    }
+    String path = await FileSaver.instance.saveFile(name: filename, ext: ext, bytes: fileContents);
+
+    if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) && context.mounted) {
+      messenger.showSnackBar(SnackBar(content: Text(localizations!.success(path))));
+    } else if (Platform.isAndroid || Platform.isIOS) {
+      if (settings.defaultExportDir.isNotEmpty) {
+        JSaver.instance.save(
+            fromPath: path,
+            androidPathOptions: AndroidPathOptions(toDefaultDirectory: true)
+        );
+      } else {
+        Share.shareXFiles([
+          XFile(
+              path,
+              mimeType: MimeType.csv.type
+          )
+        ]);
+      }
+    } else {
+      messenger.showSnackBar(const SnackBar(content: Text('UNSUPPORTED PLATFORM')));
+    }
+  }
+
+
+  Future<void> import() async {
+    final messenger = ScaffoldMessenger.of(context);
+    final localizations = AppLocalizations.of(context);
+
+    final settings = Provider.of<Settings>(context, listen: false);
+    final model = Provider.of<BloodPressureModel>(context, listen: false);
+
+    if (!([ExportFormat.csv, ExportFormat.db].contains(settings.exportFormat))) {
+      messenger.showSnackBar(SnackBar(content: Text(localizations!.errWrongImportFormat)));
+      return;
+    }
+    if (settings.exportFormat == ExportFormat.csv && !settings.exportCsvHeadline) {
+      messenger.showSnackBar(SnackBar(content: Text(localizations!.errNeedHeadline)));
+      return;
+    }
+
+    var result = await FilePicker.platform.pickFiles(
+      allowMultiple: false,
+      withData: true,
+    );
+    if (result == null) {
+      messenger.showSnackBar(SnackBar(content: Text(localizations!.errNoFileOpened)));
+      return;
+    }
+    var binaryContent = result.files.single.bytes;
+    if (binaryContent == null) {
+      messenger.showSnackBar(SnackBar(content: Text(localizations!.errCantReadFile)));
+      return;
+    }
+    var path = result.files.single.path;
+    assert(path != null); // null state directly linked to binary content
+
+    var fileContents = await ExportFileCreator(settings).parseFile(path! ,binaryContent);
+    if (fileContents == null) {
+      messenger.showSnackBar(SnackBar(content: Text(localizations!.errNotImportable)));
+      return;
+    }
+    messenger.showSnackBar(SnackBar(content: Text(localizations!.importSuccess(fileContents.length))));
+    for (final e in fileContents) {
+      model.add(e);
+    }
+  }
+}
+
 enum ExportFormat {
   csv,
   pdf,
lib/model/ram_only_implementations.dart
@@ -114,6 +114,7 @@ class RamSettings extends ChangeNotifier implements Settings {
   bool _exportLimitDataRange = false;
   MimeType _exportMimeType = MimeType.csv;
   String _defaultExportDir = '';
+  bool _exportAfterEveryEntry = false;
 
   RamSettings() {
     _accentColor = createMaterialColor(0xFF009688);
@@ -431,6 +432,15 @@ class RamSettings extends ChangeNotifier implements Settings {
     notifyListeners();
   }
 
+  @override
+  bool get exportAfterEveryEntry => _exportAfterEveryEntry;
+
+  @override
+  set exportAfterEveryEntry(bool value) {
+    _exportAfterEveryEntry = value;
+    notifyListeners();
+  }
+
   @override
   void changeStepSize(int value) {
     graphStepSize = value;
lib/model/settings_store.dart
@@ -419,6 +419,15 @@ class Settings extends ChangeNotifier {
     _prefs.setString('defaultExportDir', value);
     notifyListeners();
   }
+
+  bool get exportAfterEveryEntry {
+    return _prefs.getBool('exportAfterEveryEntry') ?? false;
+  }
+
+  set exportAfterEveryEntry(bool value) {
+    _prefs.setBool('exportAfterEveryEntry', value);
+    notifyListeners();
+  }
 }
 
 class TimeStep {
lib/screens/subsettings/export_import_screen.dart
@@ -1,19 +1,12 @@
-
-import 'dart:collection';
-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_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';
 import 'package:intl/intl.dart';
 import 'package:jsaver/jSaver.dart';
 import 'package:provider/provider.dart';
-import 'package:share_plus/share_plus.dart';
 
 class ExportImportScreen extends StatelessWidget {
   const ExportImportScreen({super.key});
@@ -40,6 +33,14 @@ class ExportImportScreen extends StatelessWidget {
                     settings.defaultExportDir = appDir.value;
                   }
                 ),
+                SwitchSettingsTile(
+                    title: Text(AppLocalizations.of(context)!.exportAfterEveryInput),
+                    description: Text(AppLocalizations.of(context)!.exportAfterEveryInputDesc),
+                    initialValue: settings.exportAfterEveryEntry,
+                    onToggle: (value) {
+                      settings.exportAfterEveryEntry = value;
+                    }
+                ),
                 DropDownSettingsTile<ExportFormat>(
                   key: const Key('exportFormat'),
                   title: Text(AppLocalizations.of(context)!.exportFormat),
@@ -286,60 +287,7 @@ class ExportImportButtons extends StatelessWidget {
                 child: MaterialButton(
                   height: 60,
                   child:  Text(AppLocalizations.of(context)!.export),
-                  onPressed: () async {
-                    var settings = Provider.of<Settings>(context, listen: false);
-
-                    final UnmodifiableListView<BloodPressureRecord> 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<BloodPressureModel>(context, listen: false).getInTimeRange(settings.exportDataRange.start, settings.exportDataRange.end);
-                    } else {
-                      entries = await Provider.of<BloodPressureModel>(context, listen: false).all;
-                    }
-                    var fileContents = await DataExporter(settings).createFile(entries);
-
-                    String filename = 'blood_press_${DateTime.now().toIso8601String()}';
-                    String ext;
-                    switch(settings.exportFormat) {
-                      case ExportFormat.csv:
-                        ext = 'CSV'; // lower case 'csv' gets automatically converted to 'csv.xls' for some reason
-                        break;
-                      case ExportFormat.pdf:
-                        ext = 'pdf';
-                        break;
-                      case ExportFormat.db:
-                        ext = 'db';
-                        break;
-                    }
-                    String path = await FileSaver.instance.saveFile(name: filename, ext: ext, 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) {
-                      if (settings.defaultExportDir.isNotEmpty) {
-                        JSaver.instance.save(
-                            fromPath: path,
-                            androidPathOptions: AndroidPathOptions(toDefaultDirectory: true)
-                        );
-                      } else {
-                        Share.shareXFiles([
-                          XFile(
-                              path,
-                              mimeType: MimeType.csv.type
-                          )
-                        ]);
-                      }
-                    } else {
-                      ScaffoldMessenger.of(context)
-                          .showSnackBar(const SnackBar(content: Text('UNSUPPORTED PLATFORM')));
-                    }
-                  },
+                  onPressed: () => Exporter(context).export(),
                 )
             ),
             const VerticalDivider(),
@@ -348,52 +296,7 @@ class ExportImportButtons extends StatelessWidget {
                 child: MaterialButton(
                   height: 60,
                   child: Text(AppLocalizations.of(context)!.import),
-                  onPressed: () async {
-                    final settings = Provider.of<Settings>(context, listen: false);
-                    if (!([ExportFormat.csv, ExportFormat.db].contains(settings.exportFormat))) {
-                      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
-                          content: Text(AppLocalizations.of(context)!.errWrongImportFormat)));
-                      return;
-                    }
-                    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(
-                      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 path = result.files.single.path;
-                    assert(path != null); // null state directly linked to binary content
-
-                    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)));
-                      return;
-                    }
-                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
-                        content: Text(AppLocalizations.of(context)!.importSuccess(fileContents.length))));
-                    var model = Provider.of<BloodPressureModel>(context, listen: false);
-                    for (final e in fileContents) {
-                      model.add(e);
-                    }
-                  },
+                  onPressed: () => Exporter(context).import(),
                 )
             ),
           ],
lib/screens/add_measurement.dart
@@ -1,5 +1,6 @@
 import 'package:blood_pressure_app/components/date_time_picker.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:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -215,11 +216,18 @@ class _AddMeasurementPageState extends State<AddMeasurementPage> {
                       const Spacer(),
                       ElevatedButton(
                           key: const Key('btnSave'),
-                          onPressed: () {
+                          onPressed: () async {
                             if (_formKey.currentState!.validate()) {
-                              Provider.of<BloodPressureModel>(context, listen: false)
-                                  .add(BloodPressureRecord(_time, _systolic, _diastolic, _pulse, _note));
-                              Navigator.of(context).pop();
+                              final settings = Provider.of<Settings>(context, listen: false);
+                              final model = Provider.of<BloodPressureModel>(context, listen: false);
+                              final exporter = Exporter(context);
+                              final navigator = Navigator.of(context);
+
+                              await model.add(BloodPressureRecord(_time, _systolic, _diastolic, _pulse, _note));
+                              if (settings.exportAfterEveryEntry) {
+                                exporter.export();
+                              }
+                              navigator.pop();
                             }
                           },
                           style: ElevatedButton.styleFrom(backgroundColor: Theme.of(context).primaryColor),
test/model/settings_test.dart
@@ -50,6 +50,7 @@ void main() {
       expect(s.exportLimitDataRange, false);
       expect(s.exportMimeType, MimeType.csv);
       expect(s.defaultExportDir.isEmpty, true);
+      expect(s.exportAfterEveryEntry, false);
 
       s.overrideWarnValues = true;
       expect(s.sysWarn, 120);
@@ -98,6 +99,7 @@ void main() {
       s.exportLimitDataRange = true;
       s.exportMimeType = MimeType.pdf;
       s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv';
+      s.exportAfterEveryEntry = true;
 
       expect(s.displayDataStart, DateTime.fromMillisecondsSinceEpoch(10000));
       expect(s.displayDataEnd, DateTime.fromMillisecondsSinceEpoch(200000));
@@ -126,6 +128,7 @@ void main() {
       expect(s.exportLimitDataRange, true);
       expect(s.exportMimeType, MimeType.pdf);
       expect(s.defaultExportDir, '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv');
+      expect(s.exportAfterEveryEntry, true);
     });
 
     test('setting fields should notify listeners and change values', () async {
@@ -166,8 +169,9 @@ void main() {
       s.exportLimitDataRange = true;
       s.exportMimeType = MimeType.pdf;
       s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv';
+      s.exportAfterEveryEntry = true;
 
-      expect(i, 30);
+      expect(i, 31);
     });
   });
 
@@ -211,6 +215,7 @@ void main() {
       expect(s.exportLimitDataRange, false);
       expect(s.exportMimeType, MimeType.csv);
       expect(s.defaultExportDir.isEmpty, true);
+      expect(s.exportAfterEveryEntry, false);
 
       s.overrideWarnValues = true;
       expect(s.sysWarn, 120);
@@ -259,6 +264,7 @@ void main() {
       s.exportLimitDataRange = true;
       s.exportMimeType = MimeType.pdf;
       s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv';
+      s.exportAfterEveryEntry = true;
 
 
       expect(s.displayDataStart, DateTime.fromMillisecondsSinceEpoch(10000));
@@ -288,6 +294,7 @@ void main() {
       expect(s.exportLimitDataRange, true);
       expect(s.exportMimeType, MimeType.pdf);
       expect(s.defaultExportDir, '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv');
+      expect(s.exportAfterEveryEntry, true);
     });
 
     test('setting fields should notify listeners and change values', () async {
@@ -328,8 +335,9 @@ void main() {
       s.exportLimitDataRange = true;
       s.exportMimeType = MimeType.pdf;
       s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv';
+      s.exportAfterEveryEntry = true;
 
-      expect(i, 30);
+      expect(i, 31);
     });
   });
 }