Commit b0e13f9

derdilla <derdilla06@gmail.com>
2023-07-31 10:25:06
implement custom export computations, this allows making more at runtime
1 parent 560a6f0
Changed files (4)
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: