Commit 062e27b

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2023-12-09 16:55:21
reimplement csv import with new classes
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent ce915e7
Changed files (5)
lib/l10n/app_en.arb
@@ -439,5 +439,36 @@
   "needlePinBarWidth": "Color thickness",
   "@needlePinBarWidth": {},
   "needlePinBarWidthDesc": "The width of the lines colored entries make on the graph.",
-  "@needlePinBarWidthDesc": {}
+  "@needlePinBarWidthDesc": {},
+  "errParseEmptyCsvFile": "There are not enough lines in the csv file to parse the record.",
+  "@errParseEmptyCsvFile": {},
+  "errParseTimeNotRestoreable": "There is no column that allows restoring a timestamp.",
+  "@errParseTimeNotRestoreable": {},
+  "errParseUnknownColumn": "There is no column with title \"{title}\".",
+  "@errParseUnknownColumn": {
+    "placeholders": {
+      "title": {
+        "type": "String"
+      }
+    }
+  },
+  "errParseLineTooShort": "Line {lineNumber} has fewer columns than the first line.",
+  "@errParseLineTooShort": {
+    "placeholders": {
+      "lineNumber": {
+        "type": "int"
+      }
+    }
+  },
+  "errParseFailedDecodingField": "Decoding field \"{fieldContent}\" in line {lineNumber} failed.",
+  "@errParseFailedDecodingField": {
+    "placeholders": {
+      "lineNumber": {
+        "type": "int"
+      },
+      "fieldContent": {
+        "type": "String"
+      }
+    }
+  }
 }
lib/model/export_import/record_parsing_result.dart
@@ -44,6 +44,7 @@ class RecordParsingErrorEmptyFile implements RecordParsingError {}
 class RecordParsingErrorTimeNotRestoreable implements RecordParsingError {}
 
 /// There is no column with this csv title that can be reversed.
+/// TODO: remove reversed limitation
 class RecordParsingErrorUnknownColumn implements RecordParsingError {
   RecordParsingErrorUnknownColumn(this.title);
   
lib/model/storage/export_columns_store.dart
@@ -46,7 +46,7 @@ class ExportColumnsManager extends ChangeNotifier { // TODO: separate ExportColu
   ExportColumn? getColumn(String identifier) => 
       firstWhere((c) => c.internalIdentifier == identifier);
   
-  // TODO test
+  // TODO test / fix
   /// Get the first of column that satisfies [test].
   /// 
   /// Checks in the order: 
lib/model/export_import.dart
@@ -28,7 +28,7 @@ extension PdfCompatability on Color {
   PdfColor toPdfColor() => PdfColor(red / 256, green / 256, blue / 256, opacity);
 }
 
-// TODO: more testing
+@Deprecated('replaced with export_import directory')
 class ExportFileCreator {
   final Settings settings;
   final ExportSettings exportSettings;
@@ -251,6 +251,7 @@ class ExportFileCreator {
   }
 }
 
+@Deprecated('replaced with export_import directory')
 class Exporter {
   final Iterable<BloodPressureRecord> data;
   final Settings settings;
lib/screens/subsettings/export_import_screen.dart
@@ -1,13 +1,19 @@
+import 'dart:convert';
+
 import 'package:blood_pressure_app/components/consistent_future_builder.dart';
 import 'package:blood_pressure_app/components/diabled.dart';
 import 'package:blood_pressure_app/components/display_interval_picker.dart';
 import 'package:blood_pressure_app/components/settings/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/export_import/csv_converter.dart';
 import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
 import 'package:blood_pressure_app/model/export_import/legacy_column.dart';
+import 'package:blood_pressure_app/model/export_import/record_parsing_result.dart';
 import 'package:blood_pressure_app/model/export_options.dart';
+import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
 import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:file_picker/file_picker.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:jsaver/jSaver.dart';
@@ -256,8 +262,66 @@ class ExportImportButtons extends StatelessWidget {
                 child: MaterialButton(
                   height: 60,
                   child: Text(localizations.import),
-                  onPressed: () async =>
-                      Exporter.load(context, [], await ExportConfigurationModel.get(localizations)).import(),
+                  onPressed: () async {
+                    final messenger = ScaffoldMessenger.of(context);
+
+                    final file = (await FilePicker.platform.pickFiles(
+                      allowMultiple: false,
+                      withData: true,
+                    ))?.files.firstOrNull;
+                    if (file == null) {
+                      showError(messenger, localizations.errNoFileOpened);
+                      return;
+                    }
+                    if (!context.mounted) return;
+                    switch(file.extension?.toLowerCase()) {
+                      case 'csv':
+                        final binaryContent = file.bytes;
+                        if (binaryContent == null) {
+                          showError(messenger, localizations.errCantReadFile);
+                          return;
+                        }
+                        final converter = CsvConverter(
+                          Provider.of<CsvExportSettings>(context, listen: false),
+                          Provider.of<ExportColumnsManager>(context, listen: false),
+                        );
+                        final result = converter.parse(utf8.decode(binaryContent));
+                        final importedRecords = result.getOr((error) {
+                          switch (error) {
+                            case RecordParsingErrorEmptyFile():
+                              showError(messenger, localizations.errParseEmptyCsvFile);
+                              break;
+                            case RecordParsingErrorTimeNotRestoreable():
+                              showError(messenger, localizations.errParseTimeNotRestoreable);
+                              break;
+                            case RecordParsingErrorUnknownColumn():
+                              showError(messenger, localizations.errParseUnknownColumn(error.title));
+                              break;
+                            case RecordParsingErrorExpectedMoreFields():
+                              showError(messenger, localizations.errParseLineTooShort(error.lineNumber));
+                              break;
+                            case RecordParsingErrorUnparsableField():
+                              showError(messenger, localizations.errParseFailedDecodingField(
+                                  error.lineNumber, error.fieldContents));
+                              break;
+                          }
+                          return null;
+                        });
+                        if (result.hasError()) return;
+                        final model = Provider.of<BloodPressureModel>(context, listen: false);
+                        for (final record in importedRecords) { // TODO: background thread
+                          await model.add(record);
+                        }
+                        messenger.showSnackBar(SnackBar(content: Text(
+                            localizations.importSuccess(importedRecords.length))));
+                        break;
+                      case 'db':
+                        // TODO
+                        break;
+                      default:
+                        showError(messenger, localizations.errWrongImportFormat);
+                    }
+                  },
                 )
             ),
           ],
@@ -265,6 +329,10 @@ class ExportImportButtons extends StatelessWidget {
       ),
     );
   }
+
+  void showError(ScaffoldMessengerState messenger, String text) =>
+    messenger.showSnackBar(SnackBar(content: Text(text)));
+
 }
 
 class ExportWarnBanner extends StatefulWidget {