Commit 97bf77a
Changed files (12)
lib/components/export_item_order.dart
@@ -45,11 +45,11 @@ class _ExportItemsCustomizerState extends State<ExportItemsCustomizer> {
shape: BoxShape.circle
),
child: IconButton(
- tooltip: 'add exportformat',
- onPressed:() {// TODO move outside of potential reusable thing
+ tooltip: AppLocalizations.of(context)!.addExportformat,
+ onPressed:() {
Navigator.of(context).push(MaterialPageRoute(builder: (context) =>
EditExportColumnPage(onValidSubmit: (value) {
- result.add(value);
+ result.addOrUpdate(value);
},)
));
},
@@ -106,8 +106,10 @@ class _ExportItemsCustomizerState extends State<ExportItemsCustomizer> {
initialDisplayName: data.columnTitle,
initialInternalName: data.internalName,
initialFormatPattern: data.formatPattern,
- onValidSubmit: (value) {
- // TODO save
+ editable: data.editable,
+ onValidSubmit: (value) async {
+ final config = await ExportConfigurationModel.get(Provider.of<Settings>(context, listen: false), AppLocalizations.of(context)!);
+ config.addOrUpdate(value);
},
)
));
lib/components/settings_widgets.dart
@@ -164,10 +164,7 @@ class SwitchSettingsTile extends StatelessWidget {
leading: leading,
description: description,
disabled: disabled,
- trailing: Theme(
- data: Theme.of(context).copyWith(useMaterial3: true),
- child: s
- ),
+ trailing: s,
);
}
}
lib/l10n/app_en.arb
@@ -368,5 +368,12 @@
"fieldFormat": "Field format",
"@fieldFormat": {},
"result": "Result:",
- "@result": {}
+ "@result": {},
+ "pulsePressure": "Pulse pressure",
+ "@pulsePressure": {},
+ "unixTimestamp" : "Unix timestamp",
+ "@unixTimestamp": {},
+ "errCantEditThis": "You can't edit this. Feel free to look at the values for creating a new entry.",
+ "addExportformat": "Add exportformat",
+ "@addExportformat": {}
}
lib/model/export_import.dart
@@ -26,6 +26,7 @@ extension PdfCompatability on Color {
}
}
+// TODO: respect new export columns
class ExportFileCreator {
final Settings settings;
final AppLocalizations localizations;
lib/model/export_options.dart
@@ -48,17 +48,31 @@ class ExportConfigurationModel {
return _instance!;
}
- List<ExportColumn> _getDefaultFormates() => [ // TODO: localizations
- 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}}')
+ List<ExportColumn> _getDefaultFormates() => [
+ ExportColumn(internalName: 'timestampUnixMs', columnTitle: localizations.unixTimestamp, formatPattern: r'$TIMESTAMP', editable: false),
+ ExportColumn(internalName: 'formattedTimestamp', columnTitle: localizations.time, formatPattern: '\$FORMAT{\$TIMESTAMP,${settings.dateFormatString}}', editable: false),
+ ExportColumn(internalName: 'systolic', columnTitle: localizations.sysLong, formatPattern: r'$SYS', editable: false),
+ ExportColumn(internalName: 'diastolic', columnTitle: localizations.diaLong, formatPattern: r'$DIA', editable: false),
+ ExportColumn(internalName: 'pulse', columnTitle: localizations.pulLong, formatPattern: r'$PUL', editable: false),
+ ExportColumn(internalName: 'notes', columnTitle: localizations.notes, formatPattern: r'$NOTE', editable: false),
+ ExportColumn(internalName: 'pulsePressure', columnTitle: localizations.pulsePressure, formatPattern: r'{{$SYS-$DIA}}', editable: false)
];
- void add(ExportColumn format) {
+ void addOrUpdate(ExportColumn format) {
+ final existingEntries = _availableFormats.where((element) => element.internalName == format.internalName);
+ if (existingEntries.isNotEmpty) {
+ assert(existingEntries.length == 1);
+ if (!existingEntries.first.editable) {
+ assert(false, 'Attempted to update non editable field. While this doesn\'t cause any direct issues, it should not be made possible through the UI.');
+ return;
+ }
+ _availableFormats.remove(existingEntries.first);
+ _availableFormats.add(format);
+ _database.update('exportStrings', {
+ 'columnTitle': format.columnTitle,
+ 'formatPattern': format.formatPattern
+ }, where: 'internalColumnName = ?', whereArgs: [format.internalName]);
+ }
_availableFormats.add(format);
_database.insert('exportStrings', {
'internalColumnName': format.internalName,
@@ -67,7 +81,8 @@ class ExportConfigurationModel {
},);
}
- UnmodifiableMapView<String, ExportColumn> get availableFormats =>
+ UnmodifiableListView<ExportColumn> get availableFormats => UnmodifiableListView(_availableFormats);
+ UnmodifiableMapView<String, ExportColumn> get availableFormatsMap =>
UnmodifiableMapView(Map.fromIterable(_availableFormats, key: (e) => e.internalName));
}
@@ -76,7 +91,7 @@ class ExportColumn {
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
+ /// Pattern to create the field contents from: TODO documentation
/// It supports inserting values for $TIMESTAMP, $SYS $DIA $PUL and $NOTE. Where $TIMESTAMP is the time since unix epoch in milliseconds.
/// To format a timestamp in the same format as the $TIMESTAMP variable, $FORMAT(<timestamp>, <formatString>).
/// It is supported to use basic mathematics inside of double brackets ("{{}}"). In case one of them is not present in the record, -1 is provided.
@@ -93,16 +108,18 @@ class ExportColumn {
/// 3. Date format
late final String formatPattern;
+ final bool editable;
+
/// Example: ExportColumn(internalColumnName: 'pulsePressure', columnTitle: 'Pulse pressure', formatPattern: '{{$SYS-$DIA}}')
- ExportColumn({required this.internalName, required this.columnTitle, required String formatPattern}) {
+ ExportColumn({required this.internalName, required this.columnTitle, required String formatPattern, this.editable = true}) {
this.formatPattern = formatPattern.replaceAll('{{}}', '');
}
- ExportColumn.fromJson(Map<String, dynamic> json) {
+ ExportColumn.fromJson(Map<String, dynamic> json, [this.editable = true]) {
ExportColumn(
internalName: json['internalColumnName'],
columnTitle: json['columnTitle'],
- formatPattern: json['formatPattern']
+ formatPattern: json['formatPattern'],
);
}
lib/model/ram_only_implementations.dart
@@ -62,7 +62,6 @@ class RamSettings extends ChangeNotifier implements Settings {
ExportFormat _exportFormat = ExportFormat.csv;
String _csvFieldDelimiter = ',';
String _csvTextDelimiter = '"';
- List<String> _exportAddableItems = ['isoUTCTime'];
bool _exportCsvHeadline = true;
bool _exportCustomEntries = false;
List<String> _exportItems = ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes'];
@@ -301,15 +300,6 @@ class RamSettings extends ChangeNotifier implements Settings {
notifyListeners();
}
- @override
- List<String> get exportAddableItems => _exportAddableItems;
-
- @override
- set exportAddableItems(List<String> value) {
- _exportAddableItems = value;
- notifyListeners();
- }
-
@override
bool get exportCsvHeadline => _exportCsvHeadline;
lib/model/settings_store.dart
@@ -42,6 +42,9 @@ class Settings extends ChangeNotifier {
if (keys.contains('exportDataRangeEndEpochMs')) {
toAwait.add(_prefs.remove('exportDataRangeEndEpochMs'));
}
+ if (keys.contains('exportAddableItems')) {
+ toAwait.add(_prefs.remove('exportAddableItems'));
+ }
for (var e in toAwait) {
await e;
@@ -443,15 +446,7 @@ class Settings extends ChangeNotifier {
_prefs.setBool('exportCustomEntries', value);
notifyListeners();
}
-
- List<String> get exportAddableItems {
- return _prefs.getStringList('exportAddableItems') ?? ['isoUTCTime'];
- }
- set exportAddableItems(List<String> value) {
- _prefs.setStringList('exportAddableItems', value);
- notifyListeners();
- }
List<String> get exportItems {
return _prefs.getStringList('exportItems') ?? ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes'];
}
lib/screens/subsettings/export_column_data.dart
@@ -8,9 +8,10 @@ class EditExportColumnPage extends StatefulWidget {
final String? initialDisplayName;
final String? initialFormatPattern;
final void Function(ExportColumn) onValidSubmit;
+ final bool editable;
const EditExportColumnPage({super.key, this.initialDisplayName, this.initialInternalName,
- this.initialFormatPattern, required this.onValidSubmit});
+ this.initialFormatPattern, required this.onValidSubmit, this.editable = true});
@override
State<EditExportColumnPage> createState() => _EditExportColumnPageState();
@@ -48,78 +49,91 @@ class _EditExportColumnPageState extends State<EditExportColumnPage> {
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;}),
+ if (!widget.editable)
+ Text(localizations.errCantEditThis),
+ Opacity(
+ opacity: widget.editable ? 1 : 0.7,
+ child: IgnorePointer(
+ ignoring: !widget.editable,
+ 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,),
+ ],
+ ),
+ ),
),
- 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(
@@ -135,7 +149,7 @@ class _EditExportColumnPageState extends State<EditExportColumnPage> {
key: const Key('btnSave'),
icon: const Icon(Icons.save),
label: Text(AppLocalizations.of(context)!.btnSave),
- onPressed: () async {
+ onPressed: (widget.editable) ? (() async {
if (_formKey.currentState?.validate() ?? false) {
widget.onValidSubmit(ExportColumn(
internalName: _internalName!,
@@ -144,7 +158,7 @@ class _EditExportColumnPageState extends State<EditExportColumnPage> {
));
Navigator.of(context).pop();
}
- },
+ }) : null,
)
],
)
lib/screens/subsettings/export_import_screen.dart
@@ -29,8 +29,8 @@ class ExportImportScreen extends StatelessWidget {
const ExportWarnBanner(),
const SizedBox(height: 15,),
Opacity(
- opacity: (settings.exportFormat == ExportFormat.db) ? 0.5 : 1, // TODO: centralize opacity when restyle
- child: const IntervalPicker(),
+ opacity: (settings.exportFormat == ExportFormat.db) ? 0.7 : 1,
+ child: IgnorePointer(ignoring: (settings.exportFormat == ExportFormat.db), child: const IntervalPicker()),
),
SettingsTile(
title: Text(AppLocalizations.of(context)!.exportDir),
@@ -125,16 +125,14 @@ class _ExportFieldCustomisationSettingState extends State<ExportFieldCustomisati
future: _future!,
onData: (context, result) {
return Consumer<Settings>(builder: (context, settings, child) {
- final formats = result.availableFormats;
+ final formats = result.availableFormats.toSet();
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);
- }
- });
+ for (final internalName in settings.exportItems) {
+ activeFields.add(formats.singleWhere((e) => e.internalName == internalName));
+ formats.removeWhere((e) => e.internalName == internalName);
+ }
+ hiddenFields = formats.toList();
return Column(
children: [
@@ -152,7 +150,6 @@ class _ExportFieldCustomisationSettingState extends State<ExportFieldCustomisati
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,9 +130,11 @@ class _AddMeasurementPageState extends State<AddMeasurementPage> {
return null;
}
),
- ValueInput(// TODO: multiline input (minlines and maxlines)
+ ValueInput(
key: const Key('txtPul'),
initialValue: (_pulse ?? '').toString(),
+ minLines: 1,
+ maxLines: 4,
hintText: AppLocalizations.of(context)!.pulLong,
basicValidation: !settings.allowMissingValues,
preValidation: (v) => _pulse = int.tryParse(v ?? ''),
@@ -216,9 +218,11 @@ class ValueInput extends StatelessWidget {
final bool basicValidation;
final void Function(String?)? preValidation;
final FormFieldValidator<String> additionalValidator;
+ final int? minLines;
+ final int? maxLines;
const ValueInput({super.key, required this.initialValue, required this.hintText, this.focusNode, this.basicValidation = true,
- this.preValidation, required this.additionalValidator});
+ this.preValidation, required this.additionalValidator, this.minLines, this.maxLines});
@override
Widget build(BuildContext context) {
@@ -226,6 +230,8 @@ class ValueInput extends StatelessWidget {
return TextFormField(
initialValue: initialValue,
decoration: InputDecoration(hintText: hintText),
+ minLines: minLines,
+ maxLines: maxLines,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
focusNode: focusNode,
lib/main.dart
@@ -18,6 +18,7 @@ void main() async {
], child: const AppRoot()));
}
+// TODO: centralize disabling
class AppRoot extends StatelessWidget {
const AppRoot({super.key});
@@ -36,8 +37,6 @@ class AppRoot extends StatelessWidget {
return MaterialApp(
title: 'Blood Pressure App',
onGenerateTitle: (context) => AppLocalizations.of(context)!.title,
- // TODO: Use Material 3 everywhere. Some components like the add button on the start page and the settings
- // switches already use it, so they need to get this theme override removed
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: settings.accentColor,
test/model/settings_test.dart
@@ -43,7 +43,6 @@ void main() {
expect(s.csvFieldDelimiter, ',');
expect(s.csvTextDelimiter, '"');
expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']);
- expect(s.exportAddableItems, ['isoUTCTime']);
expect(s.exportCsvHeadline, true);
expect(s.exportMimeType, MimeType.csv);
expect(s.defaultExportDir.isEmpty, true);
@@ -87,7 +86,6 @@ void main() {
s.graphTitlesCount = 7;
s.csvFieldDelimiter = '|';
s.csvTextDelimiter = '\'';
- s.exportAddableItems = ['timestampUnixMs'];
s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime'];
s.exportCsvHeadline = false;
s.exportMimeType = MimeType.pdf;
@@ -116,7 +114,6 @@ void main() {
expect(s.csvFieldDelimiter, '|');
expect(s.csvTextDelimiter, '\'');
expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']);
- expect(s.exportAddableItems, ['timestampUnixMs']);
expect(s.exportCsvHeadline, false);
expect(s.exportMimeType, MimeType.pdf);
expect(s.defaultExportDir, '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv');
@@ -154,7 +151,6 @@ void main() {
s.graphTitlesCount = 2;
s.csvFieldDelimiter = '|';
s.csvTextDelimiter = '\'';
- s.exportAddableItems = ['timestampUnixMs'];
s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime'];
s.exportCsvHeadline = false;
s.exportMimeType = MimeType.pdf;
@@ -163,7 +159,7 @@ void main() {
s.allowMissingValues = true;
s.language = const Locale('de');
- expect(i, 29);
+ expect(i, 28);
});
});
@@ -199,7 +195,6 @@ void main() {
expect(s.csvFieldDelimiter, ',');
expect(s.csvTextDelimiter, '"');
expect(s.exportItems, ['timestampUnixMs', 'systolic', 'diastolic', 'pulse', 'notes']);
- expect(s.exportAddableItems, ['isoUTCTime']);
expect(s.exportCsvHeadline, true);
expect(s.exportMimeType, MimeType.csv);
expect(s.defaultExportDir.isEmpty, true);
@@ -243,7 +238,6 @@ void main() {
s.graphTitlesCount = 7;
s.csvFieldDelimiter = '|';
s.csvTextDelimiter = '\'';
- s.exportAddableItems = ['timestampUnixMs'];
s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime'];
s.exportCsvHeadline = false;
s.exportMimeType = MimeType.pdf;
@@ -272,7 +266,6 @@ void main() {
expect(s.csvFieldDelimiter, '|');
expect(s.csvTextDelimiter, '\'');
expect(s.exportItems, ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime']);
- expect(s.exportAddableItems, ['timestampUnixMs']);
expect(s.exportCsvHeadline, false);
expect(s.exportMimeType, MimeType.pdf);
expect(s.defaultExportDir, '/storage/emulated/0/Android/data/com.derdilla.bloodPressureApp/files/file.csv');
@@ -310,7 +303,6 @@ void main() {
s.graphTitlesCount = 2;
s.csvFieldDelimiter = '|';
s.csvTextDelimiter = '\'';
- s.exportAddableItems = ['timestampUnixMs'];
s.exportItems = ['systolic', 'diastolic', 'pulse', 'notes', 'isoUTCTime'];
s.exportCsvHeadline = false;
s.exportMimeType = MimeType.pdf;
@@ -319,7 +311,7 @@ void main() {
s.allowMissingValues = true;
s.language = const Locale('de');
- expect(i, 29);
+ expect(i, 28);
});
});
}