diff --git a/lib/components/export_item_order.dart b/lib/components/export_item_order.dart index 2b94a241..a275fc8d 100644 --- a/lib/components/export_item_order.dart +++ b/lib/components/export_item_order.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:badges/badges.dart' as badges; import 'package:blood_pressure_app/components/consistent_future_builder.dart'; +import 'package:blood_pressure_app/model/export_import.dart'; import 'package:blood_pressure_app/model/export_options.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:blood_pressure_app/screens/subsettings/export_column_data.dart'; @@ -105,7 +106,6 @@ class _ExportItemsCustomizerState extends State { Widget _buildManagePresetsBadge(BuildContext context, ExportConfigurationModel result, {required Widget child}) { final exportConfigurations = result.exportConfigurations; - final exportConfigurationKeys = exportConfigurations.keys.toList(); return badges.Badge( position: badges.BadgePosition.topEnd(top: 3, end: 3), badgeStyle: badges.BadgeStyle( @@ -116,13 +116,18 @@ class _ExportItemsCustomizerState extends State { icon: const Icon(Icons.collections_bookmark), itemBuilder: (BuildContext context) { return [ - for (var i = 0; i< exportConfigurationKeys.length; i++) - PopupMenuItem(value: i, child: Text(exportConfigurationKeys[i])), + for (var i = 0; i< exportConfigurations.length; i++) + PopupMenuItem(value: i, child: Text(exportConfigurations[i].$1)), ]; }, onSelected: (value) { final settings = Provider.of(context, listen: false); - settings.exportItems = exportConfigurations[exportConfigurationKeys[value]]!; + if (settings.exportFormat == ExportFormat.csv) { + settings.exportItemsCsv = exportConfigurations[value].$2; + } else { + assert(settings.exportFormat == ExportFormat.pdf); + settings.exportItemsPdf = exportConfigurations[value].$2; + } }, ), child: child, diff --git a/lib/model/export_import.dart b/lib/model/export_import.dart index b741e74c..899103e9 100644 --- a/lib/model/export_import.dart +++ b/lib/model/export_import.dart @@ -61,7 +61,7 @@ class ExportFileCreator { } Uint8List createCSVFile(List records) { - final items = exportColumnsConfig.createTable(records, settings.exportCsvHeadline); + final items = exportColumnsConfig.createTable(records, ExportFormat.csv, createHeadline: settings.exportCsvHeadline); final converter = ListToCsvConverter(fieldDelimiter: settings.csvFieldDelimiter, textDelimiter: settings.csvTextDelimiter); final csvData = converter.convert(items); return Uint8List.fromList(utf8.encode(csvData)); @@ -179,7 +179,7 @@ class ExportFileCreator { } pw.Widget _buildPdfTable(List data, DateFormat dateFormatter) { - final tableData = exportColumnsConfig.createTable(data, true); + final tableData = exportColumnsConfig.createTable(data, ExportFormat.pdf, createHeadline: true); return pw.TableHelper.fromTextArray( border: null, diff --git a/lib/model/export_options.dart b/lib/model/export_options.dart index d765927d..b57b8a13 100644 --- a/lib/model/export_options.dart +++ b/lib/model/export_options.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_gen/gen_l10n/app_localizations.dart'; import 'package:function_tree/function_tree.dart'; @@ -8,6 +9,11 @@ import 'package:intl/intl.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; +class ExportFields { + static const defaultCsv = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; + static const defaultPdf = ['formattedTimestamp','systolic','diastolic','pulse','notes']; +} + class ExportConfigurationModel { static ExportConfigurationModel? _instance; @@ -17,11 +23,12 @@ class ExportConfigurationModel { final List _availableFormats = []; - Map> get exportConfigurations => { - // Not fully localized, as potemtial user added configurations can't be localized as well - localizations.default_: ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes'], - '"My Heart" export': ['DATUM', 'SYSTOLE', 'DIASTOLE', 'PULS', 'Beschreibung', 'Tags', 'Gewicht', 'Sauerstoffsättigung'], - }; + /// Format: (title, List) + List<(String, List)> get exportConfigurations => [ + // Not fully localized, as potential user added configurations can't be localized as well + (localizations.default_, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']), + ('"My Heart" export', ['DATUM', 'SYSTOLE', 'DIASTOLE', 'PULS', 'Beschreibung', 'Tags', 'Gewicht', 'Sauerstoffsättigung']), + ]; ExportConfigurationModel._create(this.settings, this.localizations); Future _asyncInit(String? dbPath, bool isFullPath) async { @@ -54,12 +61,20 @@ class ExportConfigurationModel { return _instance!; } - List getActiveExportColumns() { - List activeFields = []; - for (final internalName in settings.exportItems) { - activeFields.add(availableFormats.singleWhere((e) => e.internalName == internalName)); + List _getActiveExportColumns(ExportFormat format) { + switch (format) { + case ExportFormat.csv: + return availableFormats.where((e) => + ((settings.exportCustomEntriesCsv) ? settings.exportItemsCsv : ExportFields.defaultCsv) + .contains(e.internalName)).toList(); + case ExportFormat.pdf: + return availableFormats.where((e) => + ((settings.exportCustomEntriesPdf) ? settings.exportItemsPdf : ExportFields.defaultPdf) + .contains(e.internalName)).toList(); + default: + assert(false, 'no data selection for this one'); + return []; } - return activeFields; } List getDefaultFormates() => [ @@ -118,30 +133,15 @@ class ExportConfigurationModel { UnmodifiableMapView get availableFormatsMap => UnmodifiableMapView(Map.fromIterable(_availableFormats, key: (e) => e.internalName)); - List> createTable(List data, bool createHeadline) { - List exportItems; - if (settings.exportCustomEntries) { - exportItems = getActiveExportColumns(); - } else { - exportItems = getDefaultFormates().where((e) => ['timestampUnixMs','systolic','diastolic','pulse','notes'].contains(e.internalName)).toList(); - } - + List> createTable(List data, ExportFormat format, {bool createHeadline = true,}) { + final exportItems = _getActiveExportColumns(format); List> items = []; if (createHeadline) { - List headline = []; - for (var i = 0; i e.internalName).toList()); } - for (var record in data) { - List row = []; - for (var attribute in exportItems) { - row.add(attribute.formatRecord(record)); - } - items.add(row); - } + final dataRows = data.map((record) => exportItems.map((attribute) => attribute.formatRecord(record)).toList()); + items.addAll(dataRows); return items; } } diff --git a/lib/model/ram_only_implementations.dart b/lib/model/ram_only_implementations.dart index b16cda4c..016481cd 100644 --- a/lib/model/ram_only_implementations.dart +++ b/lib/model/ram_only_implementations.dart @@ -2,6 +2,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/export_options.dart'; import 'package:blood_pressure_app/model/settings_store.dart'; import 'package:file_saver/file_saver.dart' show MimeType; import 'package:flutter/material.dart'; @@ -310,19 +311,19 @@ class RamSettings extends ChangeNotifier implements Settings { } @override - bool get exportCustomEntries => _exportCustomEntries; + bool get exportCustomEntriesCsv => _exportCustomEntries; @override - set exportCustomEntries(bool value) { + set exportCustomEntriesCsv(bool value) { _exportCustomEntries = value; notifyListeners(); } @override - List get exportItems => _exportItems; + List get exportItemsCsv => _exportItems; @override - set exportItems(List value) { + set exportItemsCsv(List value) { _exportItems = value; notifyListeners(); } @@ -481,6 +482,32 @@ class RamSettings extends ChangeNotifier implements Settings { notifyListeners(); } + bool _exportCustomEntriesPdf = false; + + @override + bool get exportCustomEntriesPdf { + return _exportCustomEntriesPdf; + } + + @override + set exportCustomEntriesPdf(bool value) { + _exportCustomEntriesPdf = value; + notifyListeners(); + } + + List _exportItemsPdf = ExportFields.defaultPdf; + + @override + List get exportItemsPdf { + return _exportItemsPdf; + } + + @override + set exportItemsPdf(List value) { + _exportItemsPdf = value; + notifyListeners(); + } + @override void changeStepSize(TimeStep value) { graphStepSize = value; diff --git a/lib/model/settings_store.dart b/lib/model/settings_store.dart index 8c846dbd..0dbc2d64 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:blood_pressure_app/model/export_options.dart'; import 'package:file_saver/file_saver.dart' show MimeType; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -48,6 +49,14 @@ class Settings extends ChangeNotifier { if (keys.contains('exportAddableItems')) { toAwait.add(_prefs.remove('exportAddableItems')); } + if (keys.contains('exportCustomEntries')) { + await _prefs.setBool('exportCustomEntriesCsv', _prefs.getBool('exportCustomEntries') ?? false); + toAwait.add(_prefs.remove('exportCustomEntries')); + } + if (keys.contains('exportItems')) { + await _prefs.setStringList('exportItemsCsv', _prefs.getStringList('exportItems') ?? ExportFields.defaultCsv); + toAwait.add(_prefs.remove('exportItems')); + } // reset variables for new version. Necessary for reusing variable names in new version and avoid having unexpected // breaking values in the preferences @@ -452,21 +461,21 @@ class Settings extends ChangeNotifier { notifyListeners(); } - bool get exportCustomEntries { - return _prefs.getBool('exportCustomEntries') ?? false; + bool get exportCustomEntriesCsv { + return _prefs.getBool('exportCustomEntriesCsv') ?? false; } - set exportCustomEntries(bool value) { - _prefs.setBool('exportCustomEntries', value); + set exportCustomEntriesCsv(bool value) { + _prefs.setBool('exportCustomEntriesCsv', value); notifyListeners(); } - List get exportItems { - return _prefs.getStringList('exportItems') ?? ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']; + List get exportItemsCsv { + return _prefs.getStringList('exportItemsCsv') ?? ExportFields.defaultCsv; } - set exportItems(List value) { - _prefs.setStringList('exportItems', value); + set exportItemsCsv(List value) { + _prefs.setStringList('exportItemsCsv', value); notifyListeners(); } @@ -569,6 +578,7 @@ class Settings extends ChangeNotifier { notifyListeners(); } + /// whether to add a section with all entries to pdf export bool get exportPdfExportData { return _prefs.getBool('exportPdfExportData') ?? true; } @@ -586,6 +596,24 @@ class Settings extends ChangeNotifier { _prefs.setBool('startWithAddMeasurementPage', value); notifyListeners(); } + + bool get exportCustomEntriesPdf { + return _prefs.getBool('exportCustomEntriesPdf') ?? false; + } + + set exportCustomEntriesPdf(bool value) { + _prefs.setBool('exportCustomEntriesPdf', value); + notifyListeners(); + } + + List get exportItemsPdf { + return _prefs.getStringList('exportItemsPdf') ?? ExportFields.defaultPdf; + } + + set exportItemsPdf(List value) { + _prefs.setStringList('exportItemsPdf', value); + notifyListeners(); + } } enum TimeStep { diff --git a/lib/screens/subsettings/export_import_screen.dart b/lib/screens/subsettings/export_import_screen.dart index d478b1b6..57469484 100644 --- a/lib/screens/subsettings/export_import_screen.dart +++ b/lib/screens/subsettings/export_import_screen.dart @@ -192,10 +192,17 @@ class _ExportFieldCustomisationSettingState extends State(builder: (context, settings, child) { + /// whether or not the currently selected export format supports field customization + final isApplicable = (settings.exportFormat == ExportFormat.csv || settings.exportFormat == ExportFormat.pdf && + settings.exportPdfExportData); + final exportCustomEntries = (settings.exportFormat == ExportFormat.csv) ? + settings.exportCustomEntriesCsv : settings.exportCustomEntriesPdf; + final exportItems = (settings.exportFormat == ExportFormat.csv) ? settings.exportItemsCsv : settings.exportItemsPdf; + final formats = result.availableFormats.toSet(); List activeFields = []; List hiddenFields = []; - for (final internalName in settings.exportItems) { + for (final internalName in exportItems) { activeFields.add(formats.singleWhere((e) => e.internalName == internalName)); formats.removeWhere((e) => e.internalName == internalName); } @@ -205,20 +212,30 @@ class _ExportFieldCustomisationSettingState extends State e.internalName).toList(); + if (settings.exportFormat == ExportFormat.csv) { + settings.exportItemsCsv = exportItems.map((e) => e.internalName).toList(); + } else { + assert(settings.exportFormat == ExportFormat.pdf); + settings.exportItemsPdf = exportItems.map((e) => e.internalName).toList(); + } + }, ) : const SizedBox.shrink() ], @@ -284,17 +301,21 @@ class _ExportWarnBannerState extends State { final localizations = AppLocalizations.of(context)!; String? message; return Consumer(builder: (context, settings, child) { + final exportItems = (settings.exportFormat == ExportFormat.csv) ? settings.exportItemsCsv : settings.exportItemsPdf; + final exportCustomEntries = (settings.exportFormat == ExportFormat.csv) ? + settings.exportCustomEntriesCsv : settings.exportCustomEntriesPdf; + if (_showWarnBanner && ![ExportFormat.csv, ExportFormat.db].contains(settings.exportFormat) || settings.exportCsvHeadline == false || - settings.exportCustomEntries && !(['timestampUnixMs'].any((i) => settings.exportItems.contains(i))) || + exportCustomEntries && !(['timestampUnixMs'].any((i) => exportItems.contains(i))) || ![',', '|'].contains(settings.csvFieldDelimiter) || !['"', '\''].contains(settings.csvTextDelimiter) ) { message = localizations.exportWarnConfigNotImportable; - } else if (_showWarnBanner && settings.exportCustomEntries && - !(['systolic','diastolic', 'pulse', 'notes'].every((i) => settings.exportItems.contains(i)))) { + } else if (_showWarnBanner && exportCustomEntries && + !(['systolic','diastolic', 'pulse', 'notes'].every((i) => exportItems.contains(i)))) { var missingAttributes = {'systolic','diastolic', 'pulse', 'notes'}; - missingAttributes.removeWhere((e) => settings.exportItems.contains(e)); + missingAttributes.removeWhere((e) => exportItems.contains(e)); message = localizations.exportWarnNotEveryFieldExported(missingAttributes.length, missingAttributes.toString()); } diff --git a/test/model/settings_test.dart b/test/model/settings_test.dart index d45d9b4d..3d11ff24 100644 --- a/test/model/settings_test.dart +++ b/test/model/settings_test.dart @@ -44,7 +44,7 @@ void main() { expect(s.graphTitlesCount, 5); expect(s.csvFieldDelimiter, ','); expect(s.csvTextDelimiter, '"'); - expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportItemsCsv, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); expect(s.exportCsvHeadline, true); expect(s.exportMimeType, MimeType.csv); expect(s.defaultExportDir.isEmpty, true); @@ -88,7 +88,7 @@ void main() { s.graphTitlesCount = 7; s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; + s.exportItemsCsv = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportMimeType = MimeType.pdf; s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv'; @@ -115,7 +115,7 @@ void main() { expect(s.graphTitlesCount, 7); expect(s.csvFieldDelimiter, '|'); expect(s.csvTextDelimiter, '\''); - expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']); + expect(s.exportItemsCsv, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']); expect(s.exportCsvHeadline, false); expect(s.exportMimeType, MimeType.pdf); expect(s.defaultExportDir, '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv'); @@ -153,7 +153,7 @@ void main() { s.graphTitlesCount = 2; s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; + s.exportItemsCsv = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportMimeType = MimeType.pdf; s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv'; @@ -196,7 +196,7 @@ void main() { expect(s.graphTitlesCount, 5); expect(s.csvFieldDelimiter, ','); expect(s.csvTextDelimiter, '"'); - expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); + expect(s.exportItemsCsv, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']); expect(s.exportCsvHeadline, true); expect(s.exportMimeType, MimeType.csv); expect(s.defaultExportDir.isEmpty, true); @@ -240,7 +240,7 @@ void main() { s.graphTitlesCount = 7; s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; + s.exportItemsCsv = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportMimeType = MimeType.pdf; s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv'; @@ -267,7 +267,7 @@ void main() { expect(s.graphTitlesCount, 7); expect(s.csvFieldDelimiter, '|'); expect(s.csvTextDelimiter, '\''); - expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']); + expect(s.exportItemsCsv, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']); expect(s.exportCsvHeadline, false); expect(s.exportMimeType, MimeType.pdf); expect(s.defaultExportDir, '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv'); @@ -305,7 +305,7 @@ void main() { s.graphTitlesCount = 2; s.csvFieldDelimiter = '|'; s.csvTextDelimiter = '\''; - s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; + s.exportItemsCsv = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']; s.exportCsvHeadline = false; s.exportMimeType = MimeType.pdf; s.defaultExportDir = '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv';