Commit 98674ea
Changed files (9)
lib
components
dialoges
measurement_list
model
export_import
screens
subsettings
test
lib/components/dialoges/add_export_column_dialoge.dart
@@ -2,6 +2,7 @@ import 'package:blood_pressure_app/components/measurement_list/measurement_list_
import 'package:blood_pressure_app/model/blood_pressure.dart';
import 'package:blood_pressure_app/model/export_import/column.dart';
import 'package:blood_pressure_app/model/export_import/record_formatter.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:blood_pressure_app/screens/subsettings/export_import/export_field_format_documentation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -11,10 +12,12 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// For further documentation please refer to [showAddExportColumnDialoge].
class AddExportColumnDialoge extends StatefulWidget {
/// Create a widget for creating and editing a [UserColumn].
- const AddExportColumnDialoge({super.key, this.initialColumn});
+ const AddExportColumnDialoge({super.key, this.initialColumn, required this.settings});
final ExportColumn? initialColumn;
+ final Settings settings;
+
@override
State<AddExportColumnDialoge> createState() => _AddExportColumnDialogeState();
}
@@ -109,7 +112,7 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> {
final decoded = formatter.decode(text);
return Column(
children: [
- MeasurementListRow(record: record,),
+ MeasurementListRow(record: record, settings: widget.settings,),
const SizedBox(height: 8,),
const Icon(Icons.arrow_downward),
const SizedBox(height: 8,),
@@ -161,7 +164,7 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> {
/// Internal identifier and display title are generated from
/// the CSV title. There is no check whether a userColumn
/// with the generated title exists.
-Future<UserColumn?> showAddExportColumnDialoge(BuildContext context, [ExportColumn? initialColumn]) =>
+Future<UserColumn?> showAddExportColumnDialoge(BuildContext context, Settings settings, [ExportColumn? initialColumn]) =>
showDialog<UserColumn?>(context: context, builder: (context) => Dialog.fullscreen(
- child: AddExportColumnDialoge(initialColumn: initialColumn,),
+ child: AddExportColumnDialoge(initialColumn: initialColumn, settings: settings,),
));
\ No newline at end of file
lib/components/measurement_list/measurement_list.dart
@@ -5,23 +5,25 @@ import 'package:blood_pressure_app/model/storage/intervall_store.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:provider/provider.dart';
class MeasurementList extends StatelessWidget {
- const MeasurementList({super.key});
+ const MeasurementList({super.key, required this.settings});
+
+ final Settings settings;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
- const MeasurementListHeader(),
+ MeasurementListHeader(settings: settings,),
Expanded(
child: BloodPressureBuilder(
rangeType: IntervallStoreManagerLocation.mainPage,
onData: (context, data) {
return MeasurementListEntries(
- entries: data
+ entries: data,
+ settings: settings,
);
},
)
@@ -33,9 +35,11 @@ class MeasurementList extends StatelessWidget {
class MeasurementListEntries extends StatelessWidget {
+ const MeasurementListEntries({super.key, required this.entries, required this.settings});
+
final List<BloodPressureRecord> entries;
-
- const MeasurementListEntries({super.key, required this.entries});
+
+ final Settings settings;
@override
Widget build(BuildContext context) {
@@ -45,7 +49,8 @@ class MeasurementListEntries extends StatelessWidget {
itemCount: entries.length,
itemBuilder: (context, idx) {
return MeasurementListRow(
- record: entries[idx]
+ record: entries[idx],
+ settings: settings,
);
},
);
@@ -53,51 +58,49 @@ class MeasurementListEntries extends StatelessWidget {
}
class MeasurementListHeader extends StatelessWidget {
- const MeasurementListHeader({super.key});
+ const MeasurementListHeader({super.key, required this.settings});
+
+ final Settings settings;
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
- return Consumer<Settings>(
- builder: (context, settings, child) {
- return Column(
- children: [
- Row(
- children: [
- const Expanded(
- flex: 4,
- child: SizedBox()),
- Expanded(
- flex: 30,
- child: Text(localizations.sysLong,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(fontWeight: FontWeight.bold, color: settings.sysColor))),
- Expanded(
- flex: 30,
- child: Text(localizations.diaLong,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(fontWeight: FontWeight.bold, color: settings.diaColor))),
- Expanded(
- flex: 30,
- child: Text(localizations.pulLong,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(fontWeight: FontWeight.bold, color: settings.pulColor))),
- const Expanded(
- flex: 20,
- child: SizedBox()),
- ],
- ),
- const SizedBox(
- height: 10,
- ),
- Divider(
- height: 0,
- thickness: 2,
- color: Theme.of(context).colorScheme.primaryContainer,
- )
- ]);
- }
- );
+ return Column(
+ children: [
+ Row(
+ children: [
+ const Expanded(
+ flex: 4,
+ child: SizedBox()),
+ Expanded(
+ flex: 30,
+ child: Text(localizations.sysLong,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontWeight: FontWeight.bold, color: settings.sysColor))),
+ Expanded(
+ flex: 30,
+ child: Text(localizations.diaLong,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontWeight: FontWeight.bold, color: settings.diaColor))),
+ Expanded(
+ flex: 30,
+ child: Text(localizations.pulLong,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontWeight: FontWeight.bold, color: settings.pulColor))),
+ const Expanded(
+ flex: 20,
+ child: SizedBox()),
+ ],
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ Divider(
+ height: 0,
+ thickness: 2,
+ color: Theme.of(context).colorScheme.primaryContainer,
+ )
+ ]);
}
}
\ No newline at end of file
lib/components/measurement_list/measurement_list_entry.dart
@@ -7,61 +7,58 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class MeasurementListRow extends StatelessWidget {
- final BloodPressureRecord record;
+ const MeasurementListRow({super.key, required this.record, required this.settings});
- const MeasurementListRow({super.key, required this.record});
+ final BloodPressureRecord record;
+ final Settings settings;
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
- return Consumer<Settings>(
- builder: (context, settings, child) {
- final formatter = DateFormat(settings.dateFormatString);
- return ExpansionTile(
- // Leading color possible
- title: buildRow(formatter),
- childrenPadding: const EdgeInsets.only(bottom: 10),
- backgroundColor: record.needlePin?.color.withAlpha(30),
- collapsedShape: record.needlePin != null ? Border(left: BorderSide(color: record.needlePin!.color, width: 8)) : null,
- children: [
- ListTile(
- subtitle: Text(formatter.format(record.creationTime)),
- title: Text(localizations.timestamp),
- trailing: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- IconButton(
- onPressed: () async {
- final future = showAddMeasurementDialoge(context, settings, record);
- final model = Provider.of<BloodPressureModel>(context, listen: false);
- final measurement = await future;
- if (measurement == null) return;
- if (context.mounted) {
- model.addAndExport(context, measurement);
- } else {
- assert(false, 'context not mounted');
- model.add(measurement);
- }
- },
- icon: const Icon(Icons.edit),
- tooltip: localizations.edit,
- ),
- IconButton(
- onPressed: () => _deleteEntry(settings, context, localizations),
- icon: const Icon(Icons.delete),
- tooltip: localizations.delete,
- ),
- ],
+ final formatter = DateFormat(settings.dateFormatString);
+ return ExpansionTile(
+ // Leading color possible
+ title: buildRow(formatter),
+ childrenPadding: const EdgeInsets.only(bottom: 10),
+ backgroundColor: record.needlePin?.color.withAlpha(30),
+ collapsedShape: record.needlePin != null ? Border(left: BorderSide(color: record.needlePin!.color, width: 8)) : null,
+ children: [
+ ListTile(
+ subtitle: Text(formatter.format(record.creationTime)),
+ title: Text(localizations.timestamp),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ onPressed: () async {
+ final future = showAddMeasurementDialoge(context, settings, record);
+ final model = Provider.of<BloodPressureModel>(context, listen: false);
+ final measurement = await future;
+ if (measurement == null) return;
+ if (context.mounted) {
+ model.addAndExport(context, measurement);
+ } else {
+ assert(false, 'context not mounted');
+ model.add(measurement);
+ }
+ },
+ icon: const Icon(Icons.edit),
+ tooltip: localizations.edit,
+ ),
+ IconButton(
+ onPressed: () => _deleteEntry(settings, context, localizations),
+ icon: const Icon(Icons.delete),
+ tooltip: localizations.delete,
),
- ),
- if (record.notes.isNotEmpty)
- ListTile(
- title: Text(localizations.note),
- subtitle: Text(record.notes),
- )
- ],
- );
- },
+ ],
+ ),
+ ),
+ if (record.notes.isNotEmpty)
+ ListTile(
+ title: Text(localizations.note),
+ subtitle: Text(record.notes),
+ )
+ ],
);
}
lib/model/export_import/pdf_converter.dart
@@ -30,7 +30,9 @@ class PdfConverter {
/// Create a pdf from a record list.
Future<Uint8List> create(List<BloodPressureRecord> records) async {
- final pdf = pw.Document();
+ final pdf = pw.Document(
+ creator: 'Blood pressure app',
+ );
final analyzer = BloodPressureAnalyser(records.toList());
pdf.addPage(pw.MultiPage(
lib/screens/subsettings/export_import/export_column_management_screen.dart
@@ -1,6 +1,7 @@
import 'package:blood_pressure_app/components/dialoges/add_export_column_dialoge.dart';
import 'package:blood_pressure_app/model/export_import/column.dart';
import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
@@ -45,7 +46,8 @@ class ExportColumnsManagementScreen extends StatelessWidget {
IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
- final editedColumn = await showAddExportColumnDialoge(context, column);
+ final settings = Provider.of<Settings>(context, listen: false);
+ final editedColumn = await showAddExportColumnDialoge(context, settings, column);
if (editedColumn != null) {
columnsManager.addOrUpdate(editedColumn);
}
@@ -79,7 +81,8 @@ class ExportColumnsManagementScreen extends StatelessWidget {
leading: const Icon(Icons.add),
title: Text(localizations.addExportformat),
onTap: () async{
- UserColumn? editedColumn = await showAddExportColumnDialoge(context);
+ final settings = Provider.of<Settings>(context, listen: false);
+ UserColumn? editedColumn = await showAddExportColumnDialoge(context, settings);
if (editedColumn != null) {
while (columnsManager.userColumns.containsKey(editedColumn!.internalIdentifier)) {
editedColumn = UserColumn('${editedColumn.internalIdentifier}I', editedColumn.csvTitle, editedColumn.formatPattern!);
lib/screens/subsettings/export_import/export_field_format_documentation.dart
@@ -5,10 +5,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:url_launcher/url_launcher.dart';
+/// Screen to show large markdown text.
class InformationScreen extends StatelessWidget {
+ const InformationScreen({super.key, required this.text});
+
/// text in markdown format
final String text;
- const InformationScreen({super.key, required this.text});
@override
Widget build(BuildContext context) {
lib/screens/home.dart
@@ -58,7 +58,7 @@ class AppHome extends StatelessWidget {
Expanded(
child: (settings.useLegacyList) ?
LegacyMeasurementsList(context) :
- const MeasurementList()
+ MeasurementList(settings: settings,)
)
]);
}
test/ui/components/add_export_column_dialoge_test.dart
@@ -0,0 +1,159 @@
+import 'package:blood_pressure_app/components/dialoges/add_export_column_dialoge.dart';
+import 'package:blood_pressure_app/components/measurement_list/measurement_list_entry.dart';
+import 'package:blood_pressure_app/model/export_import/column.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:blood_pressure_app/screens/subsettings/export_import/export_field_format_documentation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('AddExportColumnDialoge', () {
+ testWidgets('should show everything on load', (widgetTester) async {
+ await widgetTester.pumpWidget(_materialApp(AddExportColumnDialoge(settings: Settings(),)));
+ expect(widgetTester.takeException(), isNull);
+
+ expect(find.text('SAVE'), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+ expect(find.text('CSV-title'), findsNWidgets(2));
+ expect(find.text('Field format'), findsNWidgets(2));
+ expect(find.text('Please enter a value'), findsOneWidget);
+ expect(find.text('null'), findsOneWidget);
+ expect(find.byType(MeasurementListRow), findsOneWidget);
+ expect(find.byIcon(Icons.info_outline), findsOneWidget);
+ expect(find.byIcon(Icons.arrow_downward), findsNWidgets(2));
+ });
+ testWidgets('should prefill values', (widgetTester) async {
+ await widgetTester.pumpWidget(_materialApp(
+ AddExportColumnDialoge(initialColumn: UserColumn('id', 'csvTitle', r'formatPattern$SYS'), settings: Settings(),)
+ ));
+ expect(widgetTester.takeException(), isNull);
+
+ expect(find.text('SAVE'), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+ expect(find.text('CSV-title'), findsAtLeastNWidgets(1));
+ expect(find.text('Field format'), findsAtLeastNWidgets(1));
+ expect(find.text('csvTitle'), findsOneWidget);
+ expect(find.text(r'formatPattern$SYS'), findsOneWidget);
+ expect(find.byType(MeasurementListRow), findsOneWidget);
+ expect(find.byIcon(Icons.info_outline), findsOneWidget);
+ expect(find.byIcon(Icons.arrow_downward), findsNWidgets(2));
+ });
+ testWidgets('should show preview', (widgetTester) async {
+ await widgetTester.pumpWidget(_materialApp(
+ AddExportColumnDialoge(initialColumn: UserColumn('id', 'csvTitle', r'formatPattern$SYS'), settings: Settings(),)
+ ));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.text('Please enter a value'), findsNothing);
+ expect(find.text('null'), findsNothing);
+ expect(find.textContaining('formatPattern'), findsNWidgets(2));
+ expect(find.textContaining('RowDataFieldType.sys'), findsOneWidget);
+ });
+ testWidgets('should open format Info screen', (widgetTester) async {
+ await widgetTester.pumpWidget(_materialApp(AddExportColumnDialoge(settings: Settings(),)));
+
+ expect(find.byType(InformationScreen), findsNothing);
+
+ expect(find.byIcon(Icons.info_outline), findsOneWidget);
+ await widgetTester.tap(find.byIcon(Icons.info_outline));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(InformationScreen), findsOneWidget);
+ });
+ });
+ group('showAddExportColumnDialoge', () {
+ testWidgets('should open AddExportColumnDialoge', (widgetTester) async {
+ await widgetTester.pumpWidget(_materialApp(Builder(builder: (context) => TextButton(onPressed: () =>
+ showAddExportColumnDialoge(context, Settings()), child: const Text('X')))));
+
+ expect(find.byType(AddExportColumnDialoge), findsNothing);
+ expect(find.text('X'), findsOneWidget);
+ await widgetTester.tap(find.text('X'));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(AddExportColumnDialoge), findsOneWidget);
+ });
+ testWidgets('should return null on cancel', (widgetTester) async {
+ dynamic returnedValue = false;
+ await widgetTester.pumpWidget(_materialApp(Builder(builder: (context) => TextButton(onPressed: () async =>
+ returnedValue = await showAddExportColumnDialoge(context, Settings()), child: const Text('X')))));
+ await widgetTester.tap(find.text('X'));
+ await widgetTester.pumpAndSettle();
+
+ expect(returnedValue, false);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+ await widgetTester.tap(find.byIcon(Icons.close));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(AddExportColumnDialoge), findsNothing);
+ expect(returnedValue, null);
+ });
+ testWidgets('should save entered values', (widgetTester) async {
+ dynamic returnedValue = false;
+ await widgetTester.pumpWidget(_materialApp(Builder(builder: (context) => TextButton(onPressed: () async =>
+ returnedValue = await showAddExportColumnDialoge(context, Settings()), child: const Text('X')))));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ await widgetTester.tap(find.text('X'));
+ await widgetTester.pumpAndSettle();
+ expect(returnedValue, false);
+
+ expect(find.ancestor(of: find.text(localizations.csvTitle).first, matching: find.byType(TextFormField)),
+ findsAtLeastNWidgets(1));
+ await widgetTester.enterText(
+ find.ancestor(of: find.text(localizations.csvTitle).first, matching: find.byType(TextFormField)),
+ 'testCsvTitle');
+
+ expect(find.ancestor(of: find.text(localizations.csvTitle).first, matching: find.byType(TextFormField)),
+ findsAtLeastNWidgets(1));
+ await widgetTester.enterText(
+ find.ancestor(of: find.text(localizations.fieldFormat).first, matching: find.byType(TextFormField)),
+ r'test$SYSformat');
+
+ expect(find.text(localizations.btnSave), findsOneWidget);
+ await widgetTester.tap(find.text(localizations.btnSave));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(AddExportColumnDialoge), findsNothing);
+ expect(returnedValue, isA<UserColumn>()
+ .having((p0) => p0.csvTitle, 'csvTitle', 'testCsvTitle')
+ .having((p0) => p0.formatter.formatPattern, 'formatter', r'test$SYSformat'));
+ });
+ testWidgets('should keep internalIdentifier on edit', (widgetTester) async {
+ dynamic returnedValue = false;
+ await widgetTester.pumpWidget(_materialApp(Builder(builder: (context) => TextButton(onPressed: () async =>
+ returnedValue = await showAddExportColumnDialoge(context, Settings(),
+ UserColumn('initialInternalIdentifier', 'csvTitle', 'formatPattern')
+ ), child: const Text('X')))));
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+
+ await widgetTester.tap(find.text('X'));
+ await widgetTester.pumpAndSettle();
+ expect(returnedValue, false);
+
+ expect(find.ancestor(of: find.text(localizations.csvTitle).first, matching: find.byType(TextFormField)),
+ findsAtLeastNWidgets(1));
+ await widgetTester.enterText(
+ find.ancestor(of: find.text(localizations.csvTitle).first, matching: find.byType(TextFormField)),
+ 'changedCsvTitle');
+
+ expect(find.text(localizations.btnSave), findsOneWidget);
+ await widgetTester.tap(find.text(localizations.btnSave));
+ await widgetTester.pumpAndSettle();
+
+ expect(find.byType(AddExportColumnDialoge), findsNothing);
+ expect(returnedValue, isA<UserColumn>()
+ .having((p0) => p0.internalIdentifier, 'identifier', 'userColumn.initialInternalIdentifier'));
+ });
+ });
+
+}
+
+Widget _materialApp(Widget child) {
+ return MaterialApp(
+ localizationsDelegates: const [AppLocalizations.delegate,],
+ locale: const Locale('en'),
+ home: Scaffold(body:child),
+ );
+}
\ No newline at end of file
test/ui/components/measurement_list_entry_test.dart
@@ -12,17 +12,21 @@ void main() {
group('MeasurementListRow', () {
testWidgets('should initialize without errors', (widgetTester) async {
await widgetTester.pumpWidget(_materialApp(MeasurementListRow(
- record: BloodPressureRecord(DateTime(2023), 123, 80, 60, 'test'))));
+ settings: Settings(),
+ record: BloodPressureRecord(DateTime(2023), 123, 80, 60, 'test'))));
expect(widgetTester.takeException(), isNull);
await widgetTester.pumpWidget(_materialApp(MeasurementListRow(
- record: BloodPressureRecord(DateTime.fromMillisecondsSinceEpoch(31279811), null, null, null, 'null test'))));
+ settings: Settings(),
+ record: BloodPressureRecord(DateTime.fromMillisecondsSinceEpoch(31279811), null, null, null, 'null test'))));
expect(widgetTester.takeException(), isNull);
await widgetTester.pumpWidget(_materialApp(MeasurementListRow(
- record: BloodPressureRecord(DateTime(2023), 124, 85, 63, 'color', needlePin: const MeasurementNeedlePin(Colors.cyan)))));
+ settings: Settings(),
+ record: BloodPressureRecord(DateTime(2023), 124, 85, 63, 'color', needlePin: const MeasurementNeedlePin(Colors.cyan)))));
expect(widgetTester.takeException(), isNull);
});
testWidgets('should expand correctly', (widgetTester) async {
await widgetTester.pumpWidget(_materialApp(MeasurementListRow(
+ settings: Settings(),
record: BloodPressureRecord(DateTime(2023), 123, 78, 56, 'Test texts'))));
expect(find.byIcon(Icons.expand_more), findsOneWidget);
await widgetTester.tap(find.byIcon(Icons.expand_more));
@@ -33,6 +37,7 @@ void main() {
});
testWidgets('should display correct information', (widgetTester) async {
await widgetTester.pumpWidget(_materialApp(MeasurementListRow(
+ settings: Settings(),
record: BloodPressureRecord(DateTime(2023), 123, 78, 56, 'Test text'))));
expect(find.text('123'), findsOneWidget);
expect(find.text('78'), findsOneWidget);
@@ -49,6 +54,7 @@ void main() {
});
testWidgets('should not display null values', (widgetTester) async {
await widgetTester.pumpWidget(_materialApp(MeasurementListRow(
+ settings: Settings(),
record: BloodPressureRecord(DateTime(2023), null, null, null, ''))));
expect(find.text('null'), findsNothing);
expect(find.byIcon(Icons.expand_more), findsOneWidget);
@@ -66,7 +72,6 @@ Widget _materialApp(Widget child) {
locale: const Locale('en'),
child: MultiProvider(
providers: [
- ChangeNotifierProvider(create: (_) => Settings()),
ChangeNotifierProvider(create: (_) => IntervallStoreManager(IntervallStorage(), IntervallStorage(), IntervallStorage())),
ChangeNotifierProvider<BloodPressureModel>(create: (_) => RamBloodPressureModel()),
],