Commit 387859b
Changed files (4)
lib
components
l10n
model
export_import
lib/components/dialoges/fullscreen_dialoge.dart
@@ -11,6 +11,7 @@ class FullscreenDialoge extends StatelessWidget {
this.onActionButtonPressed,
required this.bottomAppBar,
this.closeIcon = Icons.close,
+ this.actions = const <Widget>[],
});
/// The primary content of the dialoge.
@@ -41,6 +42,13 @@ class FullscreenDialoge extends StatelessWidget {
/// Setting this to false will let the app bar stay at the top.
final bool bottomAppBar;
+ /// Secondary actions to display on the app bar.
+ ///
+ /// Positioned somewhere between close and primary action button.
+ ///
+ /// Recommended to be used with [CheckboxMenuButton].
+ final List<Widget> actions;
+
@override
Widget build(BuildContext context) => Scaffold(
body: body,
@@ -59,6 +67,10 @@ class FullscreenDialoge extends StatelessWidget {
onPressed: () => Navigator.pop(context, null),
icon: Icon(closeIcon),
),
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: actions,
+ ),
actions: [
if (actionButtonText != null)
TextButton(
lib/components/dialoges/import_preview_dialoge.dart
@@ -47,6 +47,9 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
/// Whether to limit shown rows to [_kRowLimit] for faster rendering.
bool _limitRows = true;
+ /// Whether the CSV file has a title row that should be ignored.
+ bool _csvHasTitle = true;
+
@override
void initState() {
super.initState();
@@ -82,13 +85,23 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
@override
Widget build(BuildContext context) => FullscreenDialoge(
- actionButtonText: AppLocalizations.of(context)!.import,
bottomAppBar: widget.bottomAppBar,
+ actionButtonText: AppLocalizations.of(context)!.import,
onActionButtonPressed: (_showingError) ? null : () {
final result = _actor.attemptParse();
if (result.hasError()) return;
Navigator.pop<List<BloodPressureRecord>>(context, result.getOr((e) => null));
},
+ actions: [
+ CheckboxMenuButton(
+ value: _actor.hasHeadline,
+ onChanged: (state) => setState(() {
+ _actor.hasHeadline = state ?? true;
+ _actor.attemptParse();
+ }),
+ child: Text(AppLocalizations.of(context)!.titleInCsv),
+ ),
+ ],
body: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
@@ -99,7 +112,7 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
- DropdownButton( // TODO: show original column name
+ DropdownButton(
items: [
DropdownMenuItem(
child: Text(
@@ -122,7 +135,7 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
Text(parser.formatPattern!, style: Theme.of(context).textTheme.labelSmall,),
],
),
- ), // TODO: consider more info
+ ),
),
],
value: _actor.columnParsers[_actor.columnNames[colIdx]],
@@ -136,7 +149,7 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
const Divider(),
for (int rowIdx = 0; rowIdx < (_limitRows
? min(_actor.dataLines.length, _kRowLimit)
- : _actor.dataLines.length); rowIdx++) // TODO rework if needed (parsed?)
+ : _actor.dataLines.length); rowIdx++)
_buildCell(
rowIdx,
_actor.dataLines[rowIdx][colIdx],
@@ -190,8 +203,6 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
}
}
-// TODO: add setting for row limiting to make errors actionable
-
/// Shows a dialoge to preview import of a csv file
Future<List<BloodPressureRecord>?> showImportPreview(
BuildContext context,
lib/l10n/app_en.arb
@@ -502,5 +502,7 @@
"dosis": "Dosis",
"@dosis": {},
"valueDistribution": "Value distribution",
- "@valueDistribution": {}
+ "@valueDistribution": {},
+ "titleInCsv": "Title in CSV",
+ "@titleInCsv": {}
}
lib/model/export_import/csv_record_parsing_actor.dart
@@ -1,4 +1,6 @@
+import 'dart:collection';
+
import 'package:blood_pressure_app/model/export_import/column.dart';
import 'package:blood_pressure_app/model/export_import/csv_converter.dart';
import 'package:blood_pressure_app/model/export_import/record_parsing_result.dart';
@@ -8,21 +10,26 @@ class CsvRecordParsingActor {
/// Create an intermediate object to manage a record parsing process.
CsvRecordParsingActor(this._converter, String csvString) {
final lines = _converter.getCsvLines(csvString);
- _headline = lines.removeAt(0);
- dataLines = lines;
- _columnNames = _headline ?? [];
+ _firstLine = lines.removeAt(0);
+ _bodyLines = lines;
+ _columnNames = _firstLine ?? [];
_columnParsers = _converter.getColumns(_columnNames);
}
final CsvConverter _converter;
/// All lines without the first line.
- late final List<List<String>> dataLines;
-
- List<String>? _headline;
+ late final List<List<String>> _bodyLines;
+
+ /// All lines containing data.
+ UnmodifiableListView<List<String>> get dataLines {
+ final lines = _bodyLines.toList();
+ if(!hasHeadline && _firstLine != null) lines.insert(0, _firstLine!);
+ return UnmodifiableListView(lines);
+ }
/// The first line in the csv file.
- List<String>? get headline => _headline;
+ List<String>? _firstLine;
late List<String> _columnNames;
@@ -36,6 +43,9 @@ class CsvRecordParsingActor {
/// There is no guarantee that every column in [columnNames] has a parser.
Map<String, ExportColumn> get columnParsers => _columnParsers;
+ /// Whether the CSV file has a title row (first line) that contains no data.
+ bool hasHeadline = true;
+
/// Override a columns with a custom one.
void changeColumnParser(String columnName, ExportColumn? parser) {
assert(_columnNames.contains(columnName));
@@ -47,8 +57,7 @@ class CsvRecordParsingActor {
}
/// Try to parse the data with the current configuration.
- RecordParsingResult attemptParse() =>
- _converter.parseRecords(dataLines, columnNames, columnParsers, false);
+ RecordParsingResult attemptParse() {
+ return _converter.parseRecords(dataLines, columnNames, columnParsers, false);
+ }
}
-
-// TODO: consider joining headline and columnNames OR support no headline.