Commit b0e13f9
Changed files (4)
lib
components
screens
subsettings
lib/components/export_item_order.dart
@@ -0,0 +1,169 @@
+
+import 'dart:async';
+
+import 'package:blood_pressure_app/model/blood_pressure.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:function_tree/function_tree.dart';
+
+class ExportItemsCustomizer extends StatelessWidget {
+ final List<String> exportItems;
+ final List<String> exportAddableItems;
+ final FutureOr<void> Function(List<String> exportItems, List<String> exportAddableItems) onReorder;
+
+ const ExportItemsCustomizer({super.key, required this.exportItems, required this.exportAddableItems,
+ required this.onReorder});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.all(25),
+ padding: const EdgeInsets.all(20),
+ height: 420,
+ 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: _onReorderList,
+ children: <Widget>[
+ for (int i = 0; i < exportItems.length; i += 1)
+ ListTile(
+ key: Key('l_${exportItems[i]}'),
+ title: Text(exportItems[i]),
+ trailing: const Icon(Icons.drag_handle),
+ ),
+ _buildListSectionDivider(context),
+ for (int i = 0; i < exportAddableItems.length; i += 1)
+ ListTile(
+ key: Key('ul_${exportAddableItems[i]}'),
+ title: Opacity(opacity: 0.7,child: Text(exportAddableItems[i]),),
+ trailing: const Icon(Icons.drag_handle),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildListSectionDivider(BuildContext context) {
+ return IgnorePointer(
+ key: UniqueKey(),
+ child: Opacity(
+ opacity: 0.7,
+ child: Row(
+ children: <Widget>[
+ const Expanded(
+ child: Divider()
+ ),
+ Container(
+ padding: const EdgeInsets.all(10),
+ child: const Icon(Icons.arrow_downward)
+ ),
+ Text(AppLocalizations.of(context)!.exportHiddenFields),
+ Container(
+ padding: const EdgeInsets.all(10),
+ child: const Icon(Icons.arrow_downward)
+ ),
+ const Expanded(
+ child: Divider()
+ ),
+ ]
+ ),
+ ),
+ );
+ }
+
+ _onReorderList(oldIndex, newIndex) {
+ /**
+ * We have a list of items that is structured like the following:
+ * [ exportItems.length, 1, exportAddableItems.length ]
+ *
+ * So oldIndex is either (0 <= oldIndex < exportItems.length) or
+ * ((exportItems.length + 1) <= oldIndex < (exportItems.length + 1 + exportAddableItems.length))
+ * newIndex is in the range (0 <= newIndex < (exportItems.length + 1 + exportAddableItems.length))
+ *
+ * In case the entry is moved upwards on the list the new position needs to have 1 subtracted because there
+ * is an entry missing above it now.
+ *
+ * If the newIndex is (0 <= newIndex < (exportItems.length + 1)) the Item got moved above the divider.
+ * The + 1 is needed to compensate for moving the item one position above the divider and thereby replacing
+ * its index.
+ */
+ if (oldIndex < newIndex) {
+ newIndex -= 1;
+ }
+
+ final String item;
+ if (0 <= oldIndex && oldIndex < exportItems.length) {
+ item = exportItems.removeAt(oldIndex);
+ } else if ((exportItems.length + 1) <= oldIndex && oldIndex < (exportItems.length + 1 + exportAddableItems.length)) {
+ item = exportAddableItems.removeAt(oldIndex - (exportItems.length + 1));
+ } else {
+ assert(false, 'oldIndex outside expected boundaries');
+ return;
+ }
+
+ if (newIndex < (exportItems.length + 1)) {
+ exportItems.insert(newIndex, item);
+ } else {
+ newIndex -= (exportItems.length + 1);
+ exportAddableItems.insert(newIndex, item);
+ }
+
+ onReorder(exportItems, exportAddableItems);
+ }
+}
+
+class ExportColumn {
+ static const _functionRegex = r'\{\{([^}]*)}}';
+
+ /// pure name as in the title of the csv file and for internal purposes. Should not contain special characters and spaces.
+ final String internalColumnName;
+ /// Display title of the column. Possibly localized
+ final String columnTitle;
+ /// Pattern to create the field contents from: TODO implement input and documentation
+ late final String formatPattern;
+ final List<MultiVariableFunction> _parsedFunctions = [];
+
+ /// Example: ExportColumn(internalColumnName: 'pulsePressure', columnTitle: 'Pulse pressure', formatPattern: '{{SYS-DIA}}')
+ ExportColumn({required this.internalColumnName, required this.columnTitle, required String formatPattern}) {
+ this.formatPattern = formatPattern.replaceAll('{{}}', '');
+
+ final mathSnippets = RegExp(_functionRegex).allMatches(this.formatPattern);
+ for (final m in mathSnippets) {
+ assert(m.groupCount == 1, 'If a math block is found content is expected');
+ final function = m.group(0)!.toMultiVariableFunction(['SYS', 'DIA', 'PUL']);
+ _parsedFunctions.add(function);
+ }
+ }
+
+ String formatRecord(BloodPressureRecord record) {
+ var fieldContents = formatPattern;
+
+ int matchIndex = 0;
+ fieldContents = fieldContents.replaceAllMapped(RegExp(_functionRegex), (m) {
+ assert (_parsedFunctions.length > matchIndex);
+ final result = _parsedFunctions[matchIndex].call({
+ 'SYS' : record.systolic ?? -1,
+ 'DIA': record.diastolic ?? -1,
+ 'PUL': record.pulse ?? -1
+ }) as double;
+ matchIndex++;
+ return result.toString();
+ });
+
+ /*
+ fieldContents.replaceAll('\$SYS', record.systolic.toString());
+ fieldContents.replaceAll('\$DIA', record.diastolic.toString());
+ fieldContents.replaceAll('\$PUL', record.pulse.toString());
+ */
+
+ return fieldContents;
+ }
+}
+
+
+// 100 - 80 + (50 / 80 * (100 % 50))
\ No newline at end of file
lib/screens/subsettings/export_import_screen.dart
@@ -1,4 +1,5 @@
import 'package:blood_pressure_app/components/display_interval_picker.dart';
+import 'package:blood_pressure_app/components/export_item_order.dart';
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';
@@ -116,171 +117,21 @@ class ExportFieldCustomisationSetting extends StatelessWidget {
settings.exportCustomEntries = value;
}
),
- (settings.exportFormat == ExportFormat.csv && settings.exportCustomEntries) ? const CsvItemsOrderCreator() : const SizedBox.shrink()
+ (settings.exportFormat == ExportFormat.csv && settings.exportCustomEntries) ?
+ ExportItemsCustomizer(
+ exportItems: settings.exportItems,
+ exportAddableItems: settings.exportAddableItems,
+ onReorder: (exportItems, exportAddableItems) {
+ settings.exportItems = exportItems;
+ settings.exportAddableItems = exportAddableItems;
+ },
+ ) : const SizedBox.shrink()
],
);
});
}
}
-class CsvItemsOrderCreator extends StatelessWidget {
- const CsvItemsOrderCreator({super.key});
- @override
- Widget build(BuildContext context) {
- return Consumer<Settings>(builder: (context, settings, child) {
- //settings.exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes'];
- return Container(
- margin: const EdgeInsets.all(25),
- padding: const EdgeInsets.all(20),
- height: 420,
- 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) {
- /**
- * We have a list of items that is structured like the following:
- * [ exportItems.length, 1, exportAddableItems.length ]
- *
- * So oldIndex is either (0 <= oldIndex < exportItems.length) or
- * ((exportItems.length + 1) <= oldIndex < (exportItems.length + 1 + exportAddableItems.length))
- * newIndex is in the range (0 <= newIndex < (exportItems.length + 1 + exportAddableItems.length))
- *
- * In case the entry is moved upwards on the list the new position needs to have 1 subtracted because there
- * is an entry missing above it now.
- *
- * If the newIndex is (0 <= newIndex < (exportItems.length + 1)) the Item got moved above the divider.
- * The + 1 is needed to compensate for moving the item one position above the divider and thereby replacing
- * its index.
- */
- var exportItems = settings.exportItems;
- var exportAddableItems = settings.exportAddableItems;
- if (oldIndex < newIndex) { // The
- newIndex -= 1;
- }
-
- final String item;
- if (0 <= oldIndex && oldIndex < exportItems.length) {
- item = exportItems.removeAt(oldIndex);
- } else if ((exportItems.length + 1) <= oldIndex && oldIndex < (exportItems.length + 1 + exportAddableItems.length)) {
- item = exportAddableItems.removeAt(oldIndex - (exportItems.length + 1));
- } else {
- assert(false, 'oldIndex outside expected boundaries');
- return;
- }
-
- if (newIndex < (exportItems.length + 1)) {
- exportItems.insert(newIndex, item);
- } else {
- newIndex -= (exportItems.length + 1);
- exportAddableItems.insert(newIndex, item);
- }
-
- settings.exportItems = exportItems;
- settings.exportAddableItems = exportAddableItems;
- },
- /*
- footer: (settings.exportItems.length < 5) ? 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))
- ),
- child: Container(
- height: 330,
- padding: const EdgeInsets.all(30),
- child: ListView(
- children: [
- for (int i = 0; i < exportAddableItems.length; i += 1)
- ListTile(
- title: Text(exportAddableItems[i]),
- onTap: () {
- var addedItem = exportAddableItems.removeAt(i);
- exportItems.add(addedItem);
- Navigator.of(context).pop();
- settings.exportItems = exportItems;
- settings.exportAddableItems = exportAddableItems;
- },
- )
- ],
- ),
- ),
- );
- }
- );
- },
- 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,
- */
- children: <Widget>[
- for (int i = 0; i < settings.exportItems.length; i += 1)
- ListTile(
- key: Key('l_${settings.exportItems[i]}'),
- title: Text(settings.exportItems[i]),
- trailing: const Icon(Icons.drag_handle),
- ),
- _buildListSectionDivider(context),
- for (int i = 0; i < settings.exportAddableItems.length; i += 1)
- ListTile(
- key: Key('ul_${settings.exportAddableItems[i]}'),
- title: Opacity(opacity: 0.7,child: Text(settings.exportAddableItems[i]),),
- trailing: const Icon(Icons.drag_handle),
- ),
- ],
- ),
- );
- });
- }
-
- Widget _buildListSectionDivider(BuildContext context) {
- return IgnorePointer(
- key: UniqueKey(),
- child: Opacity(
- opacity: 0.7,
- child: Row(
- children: <Widget>[
- const Expanded(
- child: Divider()
- ),
- Container(
- padding: const EdgeInsets.all(10),
- child: const Icon(Icons.arrow_downward)
- ),
- Text(AppLocalizations.of(context)!.exportHiddenFields),
- Container(
- padding: const EdgeInsets.all(10),
- child: const Icon(Icons.arrow_downward)
- ),
- const Expanded(
- child: Divider()
- ),
- ]
- ),
- ),
- );
- }
-}
-
class ExportImportButtons extends StatelessWidget {
const ExportImportButtons({super.key});
pubspec.lock
@@ -269,6 +269,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ function_tree:
+ dependency: "direct main"
+ description:
+ name: function_tree
+ sha256: "204efae1786814cee404e68000aebda48bcf0419cc52b10e547c6c82f1b1da92"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.13"
glob:
dependency: transitive
description:
pubspec.yaml
@@ -30,6 +30,7 @@ dependencies:
file_saver: ^0.2.1 # BSD-3-Clause
file_picker: ^5.2.11 # MIT
jsaver: ^1.2.0
+ function_tree: ^0.8.13
dev_dependencies:
flutter_test: