Commit 1c1a8e4

derdilla <82763757+derdilla@users.noreply.github.com>
2025-02-26 17:45:15
Update to material 3 bottom buttons for input. (#541)
* Update to material 3 bottom buttons for input. This required splitting up the button bar for compatability with the flutter API. Upgrading the design fixes #517. * remove accidentally commited empty test
1 parent 4d04b59
app/lib/data_util/entry_context.dart
@@ -1,5 +1,5 @@
 import 'package:blood_pressure_app/components/confirm_deletion_dialoge.dart';
-import 'package:blood_pressure_app/features/export_import/export_button_bar.dart';
+import 'package:blood_pressure_app/features/export_import/export_button.dart';
 import 'package:blood_pressure_app/features/input/add_measurement_dialoge.dart';
 import 'package:blood_pressure_app/logging.dart';
 import 'package:blood_pressure_app/model/storage/storage.dart';
@@ -51,7 +51,7 @@ extension EntryUtils on BuildContext {
         }
         if (mounted && exportSettings.exportAfterEveryEntry) {
           read<IntervalStoreManager>().exportPage.setToMostRecentInterval();
-          performExport(this, AppLocalizations.of(this)!);
+          performExport(this);
         }
       }
     } on ProviderNotFoundException {
app/lib/features/export_import/export_button.dart
@@ -0,0 +1,114 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:blood_pressure_app/model/export_import/csv_converter.dart';
+import 'package:blood_pressure_app/model/export_import/pdf_converter.dart';
+import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
+import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:collection/collection.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:path/path.dart';
+import 'package:persistent_user_dir_access_android/persistent_user_dir_access_android.dart';
+import 'package:provider/provider.dart';
+import 'package:sqflite/sqflite.dart';
+
+/// Text button to export entries like configured in the context.
+class ExportButton extends StatelessWidget {
+  /// Create a text button to export entries like configured in the context.
+  const ExportButton({super.key});
+
+  @override
+  Widget build(BuildContext context) => TextButton.icon(
+    label: Text(AppLocalizations.of(context)!.export),
+    icon: Icon(Icons.file_download_outlined),
+    onPressed: () => performExport(context),
+  );
+}
+
+/// Perform a full export according to the configuration in [context].
+void performExport(BuildContext context) async { // TODO: extract
+  final localizations = AppLocalizations.of(context);
+  final exportSettings = Provider.of<ExportSettings>(context, listen: false);
+  final filename = 'blood_press_${DateTime.now().toIso8601String()}';
+  switch (exportSettings.exportFormat) {
+    case ExportFormat.db:
+      final path = join(await getDatabasesPath(), 'bp.db');
+      final data = await File(path).readAsBytes();
+
+      if (context.mounted) await _exportData(context, data, '$filename.db', 'application/vnd.sqlite3');
+      break;
+    case ExportFormat.csv:
+      final csvSettings = Provider.of<CsvExportSettings>(context, listen: false);
+      final exportColumnsManager = Provider.of<ExportColumnsManager>(context, listen: false);
+      final csvConverter = CsvConverter(
+        csvSettings,
+        exportColumnsManager,
+        await RepositoryProvider.of<MedicineRepository>(context).getAll(),
+      );
+      if (!context.mounted) return;
+      final csvString = csvConverter.create(await _getEntries(context));
+      final data = Uint8List.fromList(utf8.encode(csvString));
+      if (context.mounted) await _exportData(context, data, '$filename.csv', 'text/csv');
+      break;
+    case ExportFormat.pdf:
+      final pdfConverter = PdfConverter(
+          Provider.of<PdfExportSettings>(context, listen: false),
+          localizations!,
+          Provider.of<Settings>(context, listen: false),
+          Provider.of<ExportColumnsManager>(context, listen: false),
+      );
+      final pdf = await pdfConverter.create(await _getEntries(context));
+      if (context.mounted) await _exportData(context, pdf, '$filename.pdf', 'text/pdf');
+  }
+}
+
+/// Get the records that should be exported (oldest first).
+Future<List<(DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Weight?)>> _getEntries(BuildContext context) async {
+  final range = Provider.of<IntervalStoreManager>(context, listen: false).exportPage.currentRange;
+  final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
+  final noteRepo = RepositoryProvider.of<NoteRepository>(context);
+  final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
+  final weightRepo = RepositoryProvider.of<BodyweightRepository>(context);
+
+  final records = await bpRepo.get(range);
+  final notes = await noteRepo.get(range);
+  final intakes = await intakeRepo.get(range);
+  final weights = await weightRepo.get(range);
+
+  final entries = FullEntryList.merged(records, notes, intakes);
+
+  final entriesWithWeight = entries
+      .map((e) => (e.time, e.recordObj, e.noteObj, e.intakes, weights.firstWhereOrNull((w) => e.time == w.time)?.weight))
+      .toList();
+  for (final e in weights.where((w) => entriesWithWeight.firstWhereOrNull((n) => n.$1 == w.time) == null)) {
+    entriesWithWeight.add((e.time, BloodPressureRecord(time: e.time), Note(time: e.time), [], e.weight));
+  }
+
+  entriesWithWeight.sort((a, b) => a.$1.compareTo(b.$1));
+  return entriesWithWeight;
+}
+
+/// Save to default export path or share by providing binary data.
+Future<void> _exportData(BuildContext context, Uint8List data, String fullFileName, String mimeType) async {
+  final settings = Provider.of<ExportSettings>(context, listen: false);
+  if (settings.defaultExportDir.isEmpty || !Platform.isAndroid) {
+    await FilePicker.platform.saveFile(
+      type: FileType.any, // mimeType
+      fileName: fullFileName,
+      bytes: data,
+    );
+  } else {
+    const userDir = PersistentUserDirAccessAndroid();
+    await userDir.writeFile(settings.defaultExportDir, fullFileName, mimeType, data);
+  }
+}
app/lib/features/export_import/export_button_bar.dart
@@ -1,253 +0,0 @@
-import 'dart:async';
-import 'dart:collection';
-import 'dart:convert';
-import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:blood_pressure_app/features/export_import/import_preview_dialoge.dart';
-import 'package:blood_pressure_app/model/blood_pressure/model.dart';
-import 'package:blood_pressure_app/model/blood_pressure/record.dart';
-import 'package:blood_pressure_app/model/export_import/csv_converter.dart';
-import 'package:blood_pressure_app/model/export_import/csv_record_parsing_actor.dart';
-import 'package:blood_pressure_app/model/export_import/pdf_converter.dart';
-import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
-import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/interval_store.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:collection/collection.dart';
-import 'package:file_picker/file_picker.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:health_data_store/health_data_store.dart';
-import 'package:path/path.dart';
-import 'package:persistent_user_dir_access_android/persistent_user_dir_access_android.dart';
-import 'package:provider/provider.dart';
-import 'package:sqflite/sqflite.dart';
-
-/// Button row to export and import the current configuration.
-class ExportButtonBar extends StatelessWidget {
-  /// Create buttons for im- and exporting measurements.
-  const ExportButtonBar({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    final localizations = AppLocalizations.of(context)!;
-
-    return Container(
-      height: 60,
-      color: Theme.of(context).colorScheme.onInverseSurface,
-      child: Center(
-        child: Row(
-        children: [
-          Expanded(
-            flex: 50,
-            child: MaterialButton(
-              height: 60,
-              child: Text(localizations.export),
-              onPressed: () => performExport(context, localizations),
-            ),
-          ),
-          const VerticalDivider(),
-          Expanded(
-            flex: 50,
-            child: MaterialButton(
-              height: 60,
-              child: Text(localizations.import),
-              onPressed: () async {
-                final messenger = ScaffoldMessenger.of(context);
-
-                final file = (await FilePicker.platform.pickFiles(
-                  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;
-                    }
-                    if (!context.mounted) return;
-                    final converter = CsvConverter(
-                      Provider.of<CsvExportSettings>(context, listen: false),
-                      Provider.of<ExportColumnsManager>(context, listen: false),
-                      await RepositoryProvider.of<MedicineRepository>(context).getAll(),
-                    );
-                    if (!context.mounted) return;
-                    final importedRecords = await showImportPreview(
-                      context,
-                      CsvRecordParsingActor(
-                        converter,
-                        utf8.decode(binaryContent),
-                      ),
-                      Provider.of<ExportColumnsManager>(context, listen: false),
-                      Provider.of<Settings>(context, listen: false).bottomAppBars,
-                    );
-                    if (importedRecords == null || !context.mounted) return;
-                    final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
-                    final noteRepo = RepositoryProvider.of<NoteRepository>(context);
-                    final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
-                    await Future.forEach<FullEntry>(importedRecords, (e) async {
-                      if (e.sys != null || e.dia != null || e.pul != null) {
-                        await bpRepo.add(e.$1);
-                      }
-                      if (e.note != null || e.color != null) {
-                        await noteRepo.add(e.$2);
-                      }
-                      if (e.$3.isNotEmpty) {
-                        await Future.forEach(e.$3, intakeRepo.add);
-                      }
-                    });
-                    messenger.showSnackBar(SnackBar(content: Text(
-                      localizations.importSuccess(importedRecords.length),),),);
-                    break;
-                  case 'db':
-                    if (file.path == null) return;
-                    final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
-                    final noteRepo = RepositoryProvider.of<NoteRepository>(context);
-                    final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
-
-                    final List<BloodPressureRecord> records = [];
-                    final List<Note> notes = [];
-                    final List<MedicineIntake> intakes = [];
-                    try {
-                      final db = await openReadOnlyDatabase(file.path!);
-                      final importedDB = await HealthDataStore.load(db, true);
-                      records.addAll(await importedDB.bpRepo.get(DateRange.all()));
-                      notes.addAll(await importedDB.noteRepo.get(DateRange.all()));
-                      intakes.addAll(await importedDB.intakeRepo.get(DateRange.all()));
-                      await db.close();
-                    } catch (e) {
-                      // DB doesn't conform new format
-                    }
-
-                    try { // Update legacy format
-                      final model = (records.isNotEmpty || notes.isNotEmpty || intakes.isNotEmpty)
-                        ? null
-                        : await BloodPressureModel.create(dbPath: file.path!, isFullPath: true);
-                      for (final OldBloodPressureRecord oldR in (await model?.all) ?? []) {
-                        if (oldR.systolic != null || oldR.diastolic != null || oldR.pulse != null) {
-                          records.add(BloodPressureRecord(
-                            time: oldR.creationTime,
-                            sys: oldR.systolic == null ? null :Pressure.mmHg(oldR.systolic!),
-                            dia: oldR.diastolic == null ? null :Pressure.mmHg(oldR.diastolic!),
-                            pul: oldR.pulse,
-                          ));
-                        }
-                        if (oldR.notes.isNotEmpty || oldR.needlePin != null) {
-                          notes.add(Note(
-                            time: oldR.creationTime,
-                            note: oldR.notes.isEmpty ? null : oldR.notes,
-                            color: oldR.needlePin?.color.value,
-                          ));
-                        }
-                      }
-                      await model?.close();
-                    } catch (e) {
-                      // DB not importable
-                    }
-
-                    await Future.forEach(records, bpRepo.add);
-                    await Future.forEach(notes, noteRepo.add);
-                    await Future.forEach(intakes, intakeRepo.add);
-
-                    messenger.showSnackBar(SnackBar(content: Text(
-                      localizations.importSuccess(records.length),),),);
-                    break;
-                  default:
-                    _showError(messenger, localizations.errWrongImportFormat);
-                }
-              },
-            ),
-          ),
-          ],
-        ),
-      ),
-    );
-  }
-
-  void _showError(ScaffoldMessengerState messenger, String text) =>
-      messenger.showSnackBar(SnackBar(content: Text(text)));
-}
-
-/// Perform a full export according to the configuration in [context].
-void performExport(BuildContext context, [AppLocalizations? localizations]) async { // TODO: extract
-  localizations ??= AppLocalizations.of(context);
-  final exportSettings = Provider.of<ExportSettings>(context, listen: false);
-  final filename = 'blood_press_${DateTime.now().toIso8601String()}';
-  switch (exportSettings.exportFormat) {
-    case ExportFormat.db:
-      final path = join(await getDatabasesPath(), 'bp.db');
-      final data = await File(path).readAsBytes();
-
-      if (context.mounted) await _exportData(context, data, '$filename.db', 'application/vnd.sqlite3');
-      break;
-    case ExportFormat.csv:
-      final csvConverter = CsvConverter(
-        Provider.of<CsvExportSettings>(context, listen: false),
-        Provider.of<ExportColumnsManager>(context, listen: false),
-        await RepositoryProvider.of<MedicineRepository>(context).getAll(),
-      );
-      final csvString = csvConverter.create(await _getEntries(context));
-      final data = Uint8List.fromList(utf8.encode(csvString));
-      if (context.mounted) await _exportData(context, data, '$filename.csv', 'text/csv');
-      break;
-    case ExportFormat.pdf:
-      final pdfConverter = PdfConverter(
-          Provider.of<PdfExportSettings>(context, listen: false),
-          localizations!,
-          Provider.of<Settings>(context, listen: false),
-          Provider.of<ExportColumnsManager>(context, listen: false),
-      );
-      final pdf = await pdfConverter.create(await _getEntries(context));
-      if (context.mounted) await _exportData(context, pdf, '$filename.pdf', 'text/pdf');
-  }
-}
-
-/// Get the records that should be exported (oldest first).
-Future<List<(DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Weight?)>> _getEntries(BuildContext context) async {
-  final range = Provider.of<IntervalStoreManager>(context, listen: false).exportPage.currentRange;
-  final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
-  final noteRepo = RepositoryProvider.of<NoteRepository>(context);
-  final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
-  final weightRepo = RepositoryProvider.of<BodyweightRepository>(context);
-
-  final records = await bpRepo.get(range);
-  final notes = await noteRepo.get(range);
-  final intakes = await intakeRepo.get(range);
-  final weights = await weightRepo.get(range);
-
-  final entries = FullEntryList.merged(records, notes, intakes);
-
-  final entriesWithWeight = entries
-      .map((e) => (e.time, e.recordObj, e.noteObj, e.intakes, weights.firstWhereOrNull((w) => e.time == w.time)?.weight))
-      .toList();
-  for (final e in weights.where((w) => entriesWithWeight.firstWhereOrNull((n) => n.$1 == w.time) == null)) {
-    entriesWithWeight.add((e.time, BloodPressureRecord(time: e.time), Note(time: e.time), [], e.weight));
-  }
-
-  entriesWithWeight.sort((a, b) => a.$1.compareTo(b.$1));
-  return entriesWithWeight;
-}
-
-/// Save to default export path or share by providing binary data.
-Future<void> _exportData(BuildContext context, Uint8List data, String fullFileName, String mimeType) async {
-  final settings = Provider.of<ExportSettings>(context, listen: false);
-  if (settings.defaultExportDir.isEmpty || !Platform.isAndroid) {
-    await FilePicker.platform.saveFile(
-      type: FileType.any, // mimeType
-      fileName: fullFileName,
-      bytes: data,
-    );
-  } else {
-    const userDir = PersistentUserDirAccessAndroid();
-    await userDir.writeFile(settings.defaultExportDir, fullFileName, mimeType, data);
-  }
-}
app/lib/features/export_import/import_button.dart
@@ -0,0 +1,138 @@
+import 'dart:convert';
+
+import 'package:blood_pressure_app/features/export_import/import_preview_dialoge.dart';
+import 'package:blood_pressure_app/model/blood_pressure/model.dart';
+import 'package:blood_pressure_app/model/blood_pressure/record.dart';
+import 'package:blood_pressure_app/model/export_import/csv_converter.dart';
+import 'package:blood_pressure_app/model/export_import/csv_record_parsing_actor.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_bloc/flutter_bloc.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:provider/provider.dart';
+import 'package:sqflite/sqflite.dart';
+
+/// Text button to import entries like configured in the context.
+class ImportButton extends StatelessWidget {
+  /// Create text button to import entries like configured in the context.
+  const ImportButton({super.key});
+
+  @override
+  Widget build(BuildContext context) => TextButton.icon(
+    label: Text(AppLocalizations.of(context)!.import),
+    icon: Icon(Icons.file_upload_outlined),
+    onPressed: () async {
+      final localizations = AppLocalizations.of(context)!;
+      final messenger = ScaffoldMessenger.of(context);
+
+      final file = (await FilePicker.platform.pickFiles(
+        withData: true,
+      ))?.files.firstOrNull;
+      if (file == null) {
+        messenger.showSnackBar(SnackBar(content: Text(localizations.errNoFileOpened)));
+        return;
+      }
+      if (!context.mounted) return;
+      switch(file.extension?.toLowerCase()) {
+        case 'csv':
+          final binaryContent = file.bytes;
+          if (binaryContent == null) {
+            messenger.showSnackBar(SnackBar(content: Text(localizations.errCantReadFile)));
+            return;
+          }
+          if (!context.mounted) return;
+          final csvSettings = Provider.of<CsvExportSettings>(context, listen: false);
+          final exportColumnsManager = Provider.of<ExportColumnsManager>(context, listen: false);
+          final converter = CsvConverter(
+            csvSettings,
+            exportColumnsManager,
+            await RepositoryProvider.of<MedicineRepository>(context).getAll(),
+          );
+          if (!context.mounted) return;
+          final importedRecords = await showImportPreview(
+            context,
+            CsvRecordParsingActor(
+              converter,
+              utf8.decode(binaryContent),
+            ),
+            exportColumnsManager,
+            Provider.of<Settings>(context, listen: false).bottomAppBars,
+          );
+          if (importedRecords == null || !context.mounted) return;
+          final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
+          final noteRepo = RepositoryProvider.of<NoteRepository>(context);
+          final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
+          await Future.forEach<FullEntry>(importedRecords, (e) async {
+            if (e.sys != null || e.dia != null || e.pul != null) {
+              await bpRepo.add(e.$1);
+            }
+            if (e.note != null || e.color != null) {
+              await noteRepo.add(e.$2);
+            }
+            if (e.$3.isNotEmpty) {
+              await Future.forEach(e.$3, intakeRepo.add);
+            }
+          });
+          messenger.showSnackBar(SnackBar(content: Text(localizations.importSuccess(importedRecords.length))));
+          break;
+        case 'db':
+          if (file.path == null) return;
+          final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
+          final noteRepo = RepositoryProvider.of<NoteRepository>(context);
+          final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
+
+          final List<BloodPressureRecord> records = [];
+          final List<Note> notes = [];
+          final List<MedicineIntake> intakes = [];
+          try {
+            final db = await openReadOnlyDatabase(file.path!);
+            final importedDB = await HealthDataStore.load(db, true);
+            records.addAll(await importedDB.bpRepo.get(DateRange.all()));
+            notes.addAll(await importedDB.noteRepo.get(DateRange.all()));
+            intakes.addAll(await importedDB.intakeRepo.get(DateRange.all()));
+            await db.close();
+          } catch (e) {
+            // DB doesn't conform new format
+          }
+
+          try { // Update legacy format
+            final model = (records.isNotEmpty || notes.isNotEmpty || intakes.isNotEmpty)
+                ? null
+                : await BloodPressureModel.create(dbPath: file.path!, isFullPath: true);
+            for (final OldBloodPressureRecord oldR in (await model?.all) ?? []) {
+              if (oldR.systolic != null || oldR.diastolic != null || oldR.pulse != null) {
+                records.add(BloodPressureRecord(
+                  time: oldR.creationTime,
+                  sys: oldR.systolic == null ? null :Pressure.mmHg(oldR.systolic!),
+                  dia: oldR.diastolic == null ? null :Pressure.mmHg(oldR.diastolic!),
+                  pul: oldR.pulse,
+                ));
+              }
+              if (oldR.notes.isNotEmpty || oldR.needlePin != null) {
+                notes.add(Note(
+                  time: oldR.creationTime,
+                  note: oldR.notes.isEmpty ? null : oldR.notes,
+                  color: oldR.needlePin?.color.value,
+                ));
+              }
+            }
+            await model?.close();
+          } catch (e) {
+            // DB not importable
+          }
+
+          await Future.forEach(records, bpRepo.add);
+          await Future.forEach(notes, noteRepo.add);
+          await Future.forEach(intakes, intakeRepo.add);
+
+          messenger.showSnackBar(SnackBar(content: Text(localizations.importSuccess(records.length))));
+          break;
+        default:
+          messenger.showSnackBar(SnackBar(content: Text(localizations.errWrongImportFormat)));
+      }
+    },
+  );
+}
app/lib/features/settings/export_import_screen.dart
@@ -3,8 +3,9 @@ import 'dart:io';
 import 'package:blood_pressure_app/components/disabled.dart';
 import 'package:blood_pressure_app/data_util/interval_picker.dart';
 import 'package:blood_pressure_app/features/export_import/active_field_customization.dart';
-import 'package:blood_pressure_app/features/export_import/export_button_bar.dart';
+import 'package:blood_pressure_app/features/export_import/export_button.dart';
 import 'package:blood_pressure_app/features/export_import/export_warn_banner.dart';
+import 'package:blood_pressure_app/features/export_import/import_button.dart';
 import 'package:blood_pressure_app/features/settings/tiles/dropdown_list_tile.dart';
 import 'package:blood_pressure_app/features/settings/tiles/input_list_tile.dart';
 import 'package:blood_pressure_app/features/settings/tiles/number_input_list_tile.dart';
@@ -180,7 +181,10 @@ class ExportImportScreen extends StatelessWidget {
             ],
           ),
         ),),
-      bottomNavigationBar: const ExportButtonBar(),
+      persistentFooterButtons: [
+        const ExportButton(),
+        const ImportButton(),
+      ],
     );
   }
 }