Commit 5d1639a
Changed files (9)
lib
components
screens
test
model
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);
});
});
}