Commit e26f182
Changed files (6)
lib
components
l10n
model
screens
lib/components/export_item_order.dart
@@ -4,6 +4,7 @@ import 'dart:async';
import 'package:badges/badges.dart' as badges;
import 'package:blood_pressure_app/components/consistent_future_builder.dart';
import 'package:blood_pressure_app/model/export_options.dart';
+import 'package:blood_pressure_app/screens/subsettings/export_column_data.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
@@ -11,11 +12,11 @@ import 'package:provider/provider.dart';
import '../model/settings_store.dart';
class ExportItemsCustomizer extends StatefulWidget {
- final List<String> exportItems;
- final List<String> exportAddableItems;
- final FutureOr<void> Function(List<String> exportItems, List<String> exportAddableItems) onReorder;
+ final List<ExportColumn> shownItems;
+ final List<ExportColumn> hiddenItems;
+ final FutureOr<void> Function(List<ExportColumn> exportItems, List<ExportColumn> exportAddableItems) onReorder;
- const ExportItemsCustomizer({super.key, required this.exportItems, required this.exportAddableItems,
+ const ExportItemsCustomizer({super.key, required this.shownItems, required this.hiddenItems,
required this.onReorder});
@override
@@ -32,7 +33,6 @@ class _ExportItemsCustomizerState extends State<ExportItemsCustomizer> {
return ConsistentFutureBuilder(
future: _future!,
onData: (BuildContext context, ExportConfigurationModel result) {
- final formats = result.availableFormats;
return badges.Badge(
badgeStyle: badges.BadgeStyle(
badgeColor: Theme.of(context).colorScheme.background,
@@ -40,15 +40,21 @@ class _ExportItemsCustomizerState extends State<ExportItemsCustomizer> {
),
position: badges.BadgePosition.bottomEnd(bottom: 3, end: 3),
badgeContent: Container(
- decoration: BoxDecoration(
- border: Border.all(color: Theme.of(context).colorScheme.onBackground),
- shape: BoxShape.circle
- ),
- child: IconButton(
- tooltip: 'add exportformat',
- onPressed:() {},
- icon: const Icon(Icons.add),
- ),
+ decoration: BoxDecoration(
+ border: Border.all(color: Theme.of(context).colorScheme.onBackground),
+ shape: BoxShape.circle
+ ),
+ child: IconButton(
+ tooltip: 'add exportformat',
+ onPressed:() {// TODO move outside of potential reusable thing
+ Navigator.of(context).push(MaterialPageRoute(builder: (context) =>
+ EditExportColumnPage(onValidSubmit: (value) {
+ result.add(value);
+ },)
+ ));
+ },
+ icon: const Icon(Icons.add),
+ ),
),
child: Container(
margin: const EdgeInsets.all(25),
@@ -60,22 +66,26 @@ class _ExportItemsCustomizerState extends State<ExportItemsCustomizer> {
),
clipBehavior: Clip.hardEdge,
child: ReorderableListView(
- physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
onReorder: _onReorderList,
children: <Widget>[
- for (int i = 0; i < widget.exportItems.length; i += 1)
+ for (int i = 0; i < widget.shownItems.length; i += 1)
ListTile(
- key: Key('l_${widget.exportItems[i]}'),
- title: Text(formats[widget.exportItems[i]]?.columnTitle ?? widget.exportItems[i]),
- trailing: const Icon(Icons.drag_handle),
+ key: Key('l_${widget.shownItems[i].internalName}'),
+ title: Text(widget.shownItems[i].columnTitle),
+ trailing: _buildListItemTrailing( context, widget.shownItems[i]),
+ contentPadding: EdgeInsets.zero
),
_buildListSectionDivider(context),
- for (int i = 0; i < widget.exportAddableItems.length; i += 1)
+ for (int i = 0; i < widget.hiddenItems.length; i += 1)
ListTile(
- key: Key('ul_${widget.exportAddableItems[i]}'),
- title: Opacity(opacity: 0.7,child: Text(widget.exportAddableItems[i]),),
- trailing: const Icon(Icons.drag_handle),
+ key: Key('ul_${widget.hiddenItems[i].internalName}'),
+ title: Opacity(
+ opacity: 0.7,
+ child: Text(widget.hiddenItems[i].columnTitle),
+ ),
+ trailing: _buildListItemTrailing(context, widget.hiddenItems[i]),
+ contentPadding: EdgeInsets.zero
),
],
),
@@ -85,6 +95,30 @@ class _ExportItemsCustomizerState extends State<ExportItemsCustomizer> {
);
}
+ Widget _buildListItemTrailing(BuildContext context, ExportColumn data) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ onPressed: () {
+ Navigator.of(context).push(MaterialPageRoute(builder: (context) =>
+ EditExportColumnPage(
+ initialDisplayName: data.columnTitle,
+ initialInternalName: data.internalName,
+ initialFormatPattern: data.formatPattern,
+ onValidSubmit: (value) {
+ // TODO save
+ },
+ )
+ ));
+ },
+ icon: const Icon(Icons.edit)
+ ),
+ const Icon(Icons.drag_handle),
+ ],
+ );
+ }
+
Widget _buildListSectionDivider(BuildContext context) {
return IgnorePointer(
key: UniqueKey(),
@@ -133,24 +167,24 @@ class _ExportItemsCustomizerState extends State<ExportItemsCustomizer> {
newIndex -= 1;
}
- final String item;
- if (0 <= oldIndex && oldIndex < widget.exportItems.length) {
- item = widget.exportItems.removeAt(oldIndex);
- } else if ((widget.exportItems.length + 1) <= oldIndex && oldIndex < (widget.exportItems.length + 1 + widget.exportAddableItems.length)) {
- item = widget.exportAddableItems.removeAt(oldIndex - (widget.exportItems.length + 1));
+ final ExportColumn item;
+ if (0 <= oldIndex && oldIndex < widget.shownItems.length) {
+ item = widget.shownItems.removeAt(oldIndex);
+ } else if ((widget.shownItems.length + 1) <= oldIndex && oldIndex < (widget.shownItems.length + 1 + widget.hiddenItems.length)) {
+ item = widget.hiddenItems.removeAt(oldIndex - (widget.shownItems.length + 1));
} else {
assert(false, 'oldIndex outside expected boundaries');
return;
}
- if (newIndex < (widget.exportItems.length + 1)) {
- widget.exportItems.insert(newIndex, item);
+ if (newIndex < (widget.shownItems.length + 1)) {
+ widget.shownItems.insert(newIndex, item);
} else {
- newIndex -= (widget.exportItems.length + 1);
- widget.exportAddableItems.insert(newIndex, item);
+ newIndex -= (widget.shownItems.length + 1);
+ widget.hiddenItems.insert(newIndex, item);
}
- widget.onReorder(widget.exportItems, widget.exportAddableItems);
+ widget.onReorder(widget.shownItems, widget.hiddenItems);
}
}
lib/l10n/app_en.arb
@@ -358,5 +358,15 @@
"type": "String"
}
}
- }
+ },
+ "displayTitle": "Display title",
+ "@displayTitle": {},
+ "internalName": "Internal name",
+ "@internalName": {},
+ "errOnlyLatinCharactersAndArabicNumbers": "Only latin characters or arabic number allowed",
+ "@errOnlyLatinCharactersAndArabicNumbers": {},
+ "fieldFormat": "Field format",
+ "@fieldFormat": {},
+ "result": "Result:",
+ "@result": {}
}
lib/model/export_options.dart
@@ -35,7 +35,7 @@ class ExportConfigurationModel {
final existingDbEntries = await _database.rawQuery('SELECT * FROM exportStrings');
for (final e in existingDbEntries) {
- _availableFormats.add(ExportColumn(internalColumnName: e['internalColumnName'].toString(),
+ _availableFormats.add(ExportColumn(internalName: e['internalColumnName'].toString(),
columnTitle: e['columnTitle'].toString(), formatPattern: e['formatPattern'].toString()));
}
_availableFormats.addAll(_getDefaultFormates());
@@ -49,31 +49,31 @@ class ExportConfigurationModel {
}
List<ExportColumn> _getDefaultFormates() => [ // TODO: localizations
- ExportColumn(internalColumnName: 'timestampUnixMs', columnTitle: 'Unix timestamp', formatPattern: r'$TIMESTAMP'),
- ExportColumn(internalColumnName: 'formattedTimestamp', columnTitle: 'Time', formatPattern: '\$FORMAT{\$TIMESTAMP,${settings.dateFormatString}}'),
- ExportColumn(internalColumnName: 'systolic', columnTitle: 'Systolic', formatPattern: r'$SYS'),
- ExportColumn(internalColumnName: 'diastolic', columnTitle: 'Diastolic', formatPattern: r'$DIA'),
- ExportColumn(internalColumnName: 'pulse', columnTitle: 'Pulse', formatPattern: r'$PUL'),
- ExportColumn(internalColumnName: 'notes', columnTitle: 'Notes', formatPattern: r'$NOTE'),
- ExportColumn(internalColumnName: 'pulsePressure', columnTitle: 'Pulse pressure', formatPattern: r'{{$SYS-$DIA}}')
+ ExportColumn(internalName: 'timestampUnixMs', columnTitle: 'Unix timestamp', formatPattern: r'$TIMESTAMP'),
+ ExportColumn(internalName: 'formattedTimestamp', columnTitle: 'Time', formatPattern: '\$FORMAT{\$TIMESTAMP,${settings.dateFormatString}}'),
+ ExportColumn(internalName: 'systolic', columnTitle: 'Systolic', formatPattern: r'$SYS'),
+ ExportColumn(internalName: 'diastolic', columnTitle: 'Diastolic', formatPattern: r'$DIA'),
+ ExportColumn(internalName: 'pulse', columnTitle: 'Pulse', formatPattern: r'$PUL'),
+ ExportColumn(internalName: 'notes', columnTitle: 'Notes', formatPattern: r'$NOTE'),
+ ExportColumn(internalName: 'pulsePressure', columnTitle: 'Pulse pressure', formatPattern: r'{{$SYS-$DIA}}')
];
void add(ExportColumn format) {
_availableFormats.add(format);
_database.insert('exportStrings', {
- 'internalColumnName': format.internalColumnName,
+ 'internalColumnName': format.internalName,
'columnTitle': format.columnTitle,
'formatPattern': format.formatPattern
},);
}
UnmodifiableMapView<String, ExportColumn> get availableFormats =>
- UnmodifiableMapView(Map.fromIterable(_availableFormats, key: (e) => e.internalColumnName));
+ UnmodifiableMapView(Map.fromIterable(_availableFormats, key: (e) => e.internalName));
}
class ExportColumn {
/// pure name as in the title of the csv file and for internal purposes. Should not contain special characters and spaces.
- late final String internalColumnName;
+ late final String internalName;
/// Display title of the column. Possibly localized
late final String columnTitle;
/// Pattern to create the field contents from: TODO implement user input and documentation
@@ -94,20 +94,20 @@ class ExportColumn {
late final String formatPattern;
/// Example: ExportColumn(internalColumnName: 'pulsePressure', columnTitle: 'Pulse pressure', formatPattern: '{{$SYS-$DIA}}')
- ExportColumn({required this.internalColumnName, required this.columnTitle, required String formatPattern}) {
+ ExportColumn({required this.internalName, required this.columnTitle, required String formatPattern}) {
this.formatPattern = formatPattern.replaceAll('{{}}', '');
}
ExportColumn.fromJson(Map<String, dynamic> json) {
ExportColumn(
- internalColumnName: json['internalColumnName'],
+ internalName: json['internalColumnName'],
columnTitle: json['columnTitle'],
formatPattern: json['formatPattern']
);
}
Map<String, dynamic> toJson() => {
- 'internalColumnName': internalColumnName,
+ 'internalColumnName': internalName,
'columnTitle': columnTitle,
'formatPattern': formatPattern
};
@@ -144,6 +144,6 @@ class ExportColumn {
@override
String toString() {
- return 'ExportColumn{internalColumnName: $internalColumnName, columnTitle: $columnTitle, formatPattern: $formatPattern}';
+ return 'ExportColumn{internalColumnName: $internalName, columnTitle: $columnTitle, formatPattern: $formatPattern}';
}
}
\ No newline at end of file
lib/screens/subsettings/export_column_data.dart
@@ -0,0 +1,159 @@
+import 'package:blood_pressure_app/model/blood_pressure.dart';
+import 'package:blood_pressure_app/model/export_options.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class EditExportColumnPage extends StatefulWidget {
+ final String? initialInternalName;
+ final String? initialDisplayName;
+ final String? initialFormatPattern;
+ final void Function(ExportColumn) onValidSubmit;
+
+ const EditExportColumnPage({super.key, this.initialDisplayName, this.initialInternalName,
+ this.initialFormatPattern, required this.onValidSubmit});
+
+ @override
+ State<EditExportColumnPage> createState() => _EditExportColumnPageState();
+}
+
+class _EditExportColumnPageState extends State<EditExportColumnPage> {
+ final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+ String? _internalName;
+ String? _displayName;
+ String? _formatPattern;
+ bool _editedInternalName = false;
+ var _internalNameKeyNr = 0;
+
+
+ @override
+ void initState() {
+ super.initState();
+ _internalName = widget.initialInternalName;
+ _displayName = widget.initialDisplayName;
+ _formatPattern= widget.initialFormatPattern;
+ }
+
+ _EditExportColumnPageState();
+
+ @override
+ Widget build(BuildContext context) {
+ final localizations = AppLocalizations.of(context)!;
+ return Scaffold(
+ body: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(60.0),
+ child: Form(
+ key: _formKey,
+ child: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextFormField(
+ key: const Key('displayName'),
+ initialValue: _displayName,
+ decoration: InputDecoration(hintText: localizations.displayTitle),
+ onChanged: (String? value) {
+ if (value != null && value.isNotEmpty) {
+ setState(() {
+ _displayName = value;
+ });
+ if (_editedInternalName) return;
+ final asciiName = value.replaceAll(RegExp(r'[^A-Za-z0-9 ]'), '');
+ final internalName = asciiName.replaceAllMapped(RegExp(r' (.)'), (match) {
+ return match.group(1)!.toUpperCase();
+ }).replaceAll(' ', '');
+ setState(() {
+ _internalNameKeyNr++;
+ _internalName = internalName;
+ });
+ }
+ },
+ ),
+ TextFormField(
+ key: Key('internalName$_internalNameKeyNr'), // it should update when display name is changed without unfocussing on edit
+ initialValue: _internalName,
+ decoration: InputDecoration(hintText: localizations.internalName),
+ validator: (String? value) {
+ if (value == null || value.isEmpty || RegExp(r'[^A-Za-z0-9]').hasMatch(value)) {
+ return localizations.errOnlyLatinCharactersAndArabicNumbers;
+ } // TODO: check if one with this name already exists
+ return null;
+ },
+ onChanged: (String? value) {
+ if (value != null && value.isNotEmpty && !RegExp(r'[^A-Za-z0-9]').hasMatch(value)) {
+ setState(() {
+ _internalName = value;
+ _editedInternalName = true;
+ });
+ }
+ },
+ ),
+ TextFormField( // TODO: documentation
+ key: const Key('formatPattern'),
+ initialValue: _formatPattern,
+ decoration: InputDecoration(hintText: localizations.fieldFormat),
+ maxLines: 6,
+ minLines: 1,
+ validator: (String? value) {
+ if (value == null || value.isEmpty) {
+ return AppLocalizations.of(context)!.errNoValue;
+ } else if (_internalName != null && _displayName != null) {
+ try {
+ final column = ExportColumn(internalName: _internalName!, columnTitle: _displayName!, formatPattern: value);
+ column.formatRecord(BloodPressureRecord(DateTime.now(), 100, 80, 60, ''));
+ _formatPattern = value;
+ } catch (e) {
+ _formatPattern = null;
+ return e.toString();
+ }
+ }
+ return null;
+ },
+ onChanged: (value) => setState(() {_formatPattern = value;}),
+ ),
+ const SizedBox(height: 12,),
+ Text(localizations.result),
+ Text(((){try {
+ final column = ExportColumn(internalName: _internalName!, columnTitle: _displayName!, formatPattern: _formatPattern!);
+ return column.formatRecord(BloodPressureRecord(DateTime.now(), 100, 80, 60, 'test'));
+ } catch (e) {
+ return '-';
+ }})()),
+ const SizedBox(height: 24,),
+ Row(
+ children: [
+ TextButton(
+ key: const Key('btnCancel'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+
+ child: Text(AppLocalizations.of(context)!.btnCancel)
+ ),
+ const Spacer(),
+ FilledButton.icon(
+ key: const Key('btnSave'),
+ icon: const Icon(Icons.save),
+ label: Text(AppLocalizations.of(context)!.btnSave),
+ onPressed: () async {
+ if (_formKey.currentState?.validate() ?? false) {
+ widget.onValidSubmit(ExportColumn(
+ internalName: _internalName!,
+ columnTitle: _displayName!,
+ formatPattern: _formatPattern!
+ ));
+ Navigator.of(context).pop();
+ }
+ },
+ )
+ ],
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+ }
+}
\ No newline at end of file
lib/screens/subsettings/export_import_screen.dart
@@ -1,3 +1,4 @@
+import 'package:blood_pressure_app/components/consistent_future_builder.dart';
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';
@@ -9,6 +10,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:jsaver/jSaver.dart';
import 'package:provider/provider.dart';
+import '../../model/export_options.dart';
+
class ExportImportScreen extends StatelessWidget {
const ExportImportScreen({super.key});
@@ -101,34 +104,62 @@ class ExportImportScreen extends StatelessWidget {
}
}
-class ExportFieldCustomisationSetting extends StatelessWidget {
+class ExportFieldCustomisationSetting extends StatefulWidget {
const ExportFieldCustomisationSetting({super.key});
+ @override
+ State<ExportFieldCustomisationSetting> createState() => _ExportFieldCustomisationSettingState();
+}
+
+class _ExportFieldCustomisationSettingState extends State<ExportFieldCustomisationSetting> {
+ // hack so that FutureBuilder doesn't always rebuild
+ Future<ExportConfigurationModel>? _future;
+
@override
Widget build(BuildContext context) {
- return Consumer<Settings>(builder: (context, settings, child) {
- return Column(
- children: [
- SwitchSettingsTile(
- title: Text(AppLocalizations.of(context)!.exportCustomEntries),
- initialValue: settings.exportCustomEntries,
- disabled: settings.exportFormat != ExportFormat.csv,
- onToggle: (value) {
- settings.exportCustomEntries = value;
- }
- ),
- (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()
- ],
- );
- });
+ _future ??= ExportConfigurationModel.get(Provider.of<Settings>(context, listen: false), AppLocalizations.of(context)!);
+
+
+
+ return ConsistentFutureBuilder(
+ future: _future!,
+ onData: (context, result) {
+ return Consumer<Settings>(builder: (context, settings, child) {
+ final formats = result.availableFormats;
+ List<ExportColumn> activeFields = [];
+ List<ExportColumn> hiddenFields = [];
+ formats.forEach((internalName, e) { // todo: maintain ordering of exportItems
+ if (settings.exportItems.contains(internalName)) {
+ activeFields.add(e);
+ } else {
+ hiddenFields.add(e);
+ }
+ });
+
+ return Column(
+ children: [
+ SwitchSettingsTile(
+ title: Text(AppLocalizations.of(context)!.exportCustomEntries),
+ initialValue: settings.exportCustomEntries,
+ disabled: settings.exportFormat != ExportFormat.csv,
+ onToggle: (value) {
+ settings.exportCustomEntries = value;
+ }
+ ),
+ (settings.exportFormat == ExportFormat.csv && settings.exportCustomEntries) ?
+ ExportItemsCustomizer(
+ shownItems: activeFields,
+ hiddenItems: hiddenFields,
+ onReorder: (exportItems, exportAddableItems) {
+ settings.exportItems = exportItems.map((e) => e.internalName).toList();
+ //settings.exportAddableItems = exportAddableItems; // todo remove from data
+ },
+ ) : const SizedBox.shrink()
+ ],
+ );
+ });
+ }
+ );
}
}
lib/screens/add_measurement.dart
@@ -130,7 +130,7 @@ class _AddMeasurementPageState extends State<AddMeasurementPage> {
return null;
}
),
- ValueInput(
+ ValueInput(// TODO: multiline input (minlines and maxlines)
key: const Key('txtPul'),
initialValue: (_pulse ?? '').toString(),
hintText: AppLocalizations.of(context)!.pulLong,