Commit 2577acb
Changed files (82)
lib
components
dialoges
settings
model
blood_pressure
export_import
storage
platform_integration
screens
test
model
ui
components
settings
lib/components/dialoges/add_export_column_dialoge.dart
@@ -19,18 +19,24 @@ class AddExportColumnDialoge extends StatefulWidget {
required this.settings,
});
+ /// Prefills the form to a submitted state.
+ ///
+ /// When this is null it is assumed creating a new column is intended.
final ExportColumn? initialColumn;
+ /// Settings to determine general behavior.
final Settings settings;
@override
State<AddExportColumnDialoge> createState() => _AddExportColumnDialogeState();
}
-class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> with SingleTickerProviderStateMixin {
+class _AddExportColumnDialogeState extends State<AddExportColumnDialoge>
+ with SingleTickerProviderStateMixin {
final formKey = GlobalKey<FormState>();
- /// Csv column title used to compute internal identifier in case [widget.initialColumn] is null.
+ /// Csv column title used to compute internal identifier in case
+ /// [AddExportColumnDialoge.initialColumn] is null.
late String csvTitle;
/// Pattern for record formatting preview and for column creation on save.
@@ -82,7 +88,8 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> with Si
body: GestureDetector(
onHorizontalDragEnd: (details) {
if (details.primaryVelocity == null) return;
- if (details.primaryVelocity! < -500 && type == _FormatterType.record) {
+ if (details.primaryVelocity! < -500
+ && type == _FormatterType.record) {
_changeMode(_FormatterType.time);
}
if (details.primaryVelocity! > 500 && type == _FormatterType.time) {
@@ -98,7 +105,9 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> with Si
decoration: InputDecoration(
labelText: localizations.csvTitle,
),
- validator: (value) => (value != null && value.isNotEmpty) ? null : localizations.errNoValue,
+ validator: (value) => (value != null && value.isNotEmpty)
+ ? null
+ : localizations.errNoValue,
onSaved: (value) => setState(() {csvTitle = value!;}),
),
const SizedBox(height: 8,),
@@ -152,18 +161,35 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> with Si
borderRadius: BorderRadius.circular(20),
),
child: (){
- final record = BloodPressureRecord(DateTime.now(), 123, 78, 65, 'test note');
- final formatter = (type == _FormatterType.record) ? ScriptedFormatter(recordPattern ?? '')
+ final record = BloodPressureRecord(
+ DateTime.now(),
+ 123, 78, 65,
+ 'test note',
+ );
+ final formatter = (type == _FormatterType.record)
+ ? ScriptedFormatter(recordPattern ?? '')
: ScriptedTimeFormatter(timePattern ?? '');
final text = formatter.encode(record);
final decoded = formatter.decode(text);
return Column(
children: [
- if (type == _FormatterType.record) MeasurementListRow(record: record, settings: widget.settings,) else Text(DateFormat('MMM d, y - h:m.s').format(record.creationTime)),
+ if (type == _FormatterType.record)
+ MeasurementListRow(
+ record: record,
+ settings: widget.settings,
+ ) else Text(
+ DateFormat('MMM d, y - h:m.s')
+ .format(record.creationTime),
+ ),
const SizedBox(height: 8,),
const Icon(Icons.arrow_downward),
const SizedBox(height: 8,),
- if (text.isNotEmpty) Text(text) else Text(localizations.errNoValue, style: const TextStyle(fontStyle: FontStyle.italic),),
+ if (text.isNotEmpty)
+ Text(text)
+ else
+ Text(localizations.errNoValue,
+ style: const TextStyle(fontStyle: FontStyle.italic),
+ ),
const SizedBox(height: 8,),
const Icon(Icons.arrow_downward),
const SizedBox(height: 8,),
@@ -196,7 +222,10 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> with Si
suffixIcon: IconButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
- builder: (context) => InformationScreen(text: inputDocumentation),),);
+ builder: (context) => InformationScreen(
+ text: inputDocumentation,
+ ),
+ ),);
},
icon: const Icon(Icons.info_outline),
),
@@ -206,31 +235,38 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> with Si
],
);
- Column _createRecordFormatInput(AppLocalizations localizations, BuildContext context) =>
- _createFormatInput(localizations,
- context,
- localizations.fieldFormat,
- localizations.exportFieldFormatDocumentation,
- recordPattern ?? '',
- (value) => setState(() {
- recordPattern = value;
- }),
- (value) => (type == _FormatterType.time || value != null && value.isNotEmpty) ? null
- : localizations.errNoValue,
- );
+ Column _createRecordFormatInput(
+ AppLocalizations localizations,
+ BuildContext context,
+ ) => _createFormatInput(localizations,
+ context,
+ localizations.fieldFormat,
+ localizations.exportFieldFormatDocumentation,
+ recordPattern ?? '',
+ (value) => setState(() {
+ recordPattern = value;
+ }),
+ (value) => type == _FormatterType.time || value != null && value.isNotEmpty
+ ? null
+ : localizations.errNoValue,
+ );
- Column _createTimeFormatInput(AppLocalizations localizations, BuildContext context) =>
- _createFormatInput(localizations,
- context,
- localizations.timeFormat,
- localizations.enterTimeFormatDesc,
- timePattern ?? '',
- (value) => setState(() {
- timePattern = value;
- }),
- (value) => (type == _FormatterType.record || (value != null && value.isNotEmpty)) ? null
- : localizations.errNoValue,
- );
+ Column _createTimeFormatInput(
+ AppLocalizations localizations,
+ BuildContext context,
+ ) => _createFormatInput(localizations,
+ context,
+ localizations.timeFormat,
+ localizations.enterTimeFormatDesc,
+ timePattern ?? '',
+ (value) => setState(() {
+ timePattern = value;
+ }),
+ (value) => (type == _FormatterType.record
+ || (value != null && value.isNotEmpty))
+ ? null
+ : localizations.errNoValue,
+ );
void _saveForm() {
if (formKey.currentState?.validate() ?? false) {
@@ -239,14 +275,20 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge> with Si
if (type == _FormatterType.record) {
assert(recordPattern != null, 'validator should check');
column = (widget.initialColumn != null)
- ? UserColumn.explicit(widget.initialColumn!.internalIdentifier, csvTitle, recordPattern!)
+ ? UserColumn.explicit(
+ widget.initialColumn!.internalIdentifier,
+ csvTitle,
+ recordPattern!,)
: UserColumn(csvTitle, csvTitle, recordPattern!);
Navigator.pop(context, column);
} else {
assert(type == _FormatterType.time);
assert(timePattern != null, 'validator should check');
column = (widget.initialColumn != null)
- ? TimeColumn.explicit(widget.initialColumn!.internalIdentifier, csvTitle, timePattern!)
+ ? TimeColumn.explicit(
+ widget.initialColumn!.internalIdentifier,
+ csvTitle,
+ timePattern!,)
: TimeColumn(csvTitle, timePattern!);
Navigator.pop(context, column);
}
@@ -272,7 +314,8 @@ enum _FormatterType {
time,
}
-/// Shows a dialoge containing a export column editor to create a [UserColumn] or [TimeColumn].
+/// Shows a dialoge containing a export column editor to create a [UserColumn]
+/// or [TimeColumn].
///
/// In case [initialColumn] is null fields are initially empty.
/// When initialColumn is provided, it is ensured that the
@@ -285,7 +328,15 @@ enum _FormatterType {
/// Internal identifier and display title are generated from
/// the CSV title. There is no check whether a userColumn
/// with the generated title exists.
-Future<ExportColumn?> showAddExportColumnDialoge(BuildContext context, Settings settings, [ExportColumn? initialColumn]) =>
- showDialog<ExportColumn?>(context: context, builder: (context) => Dialog.fullscreen(
- child: AddExportColumnDialoge(initialColumn: initialColumn, settings: settings,),
- ),);
\ No newline at end of file
+Future<ExportColumn?> showAddExportColumnDialoge(
+ BuildContext context,
+ Settings settings, [
+ ExportColumn? initialColumn,
+]) => showDialog<ExportColumn?>(context: context,
+ builder: (context) => Dialog.fullscreen(
+ child: AddExportColumnDialoge(
+ initialColumn: initialColumn,
+ settings: settings,
+ ),
+ ),
+);
lib/components/dialoges/add_measurement_dialoge.dart
@@ -82,7 +82,9 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
super.initState();
time = widget.initialRecord?.creationTime ?? DateTime.now();
needlePin = widget.initialRecord?.needlePin;
- sysController = TextEditingController(text: (widget.initialRecord?.systolic ?? '').toString());
+ sysController = TextEditingController(
+ text: (widget.initialRecord?.systolic ?? '').toString(),
+ );
sysFocusNode.requestFocus();
ServicesBinding.instance.keyboard.addHandler(_onKey);
@@ -162,9 +164,11 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
focusNode: focusNode,
onSaved: onSaved,
controller: controller,
- inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
+ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (String? value) {
- if (value != null && value.isNotEmpty && (int.tryParse(value) ?? -1) > 40) {
+ if (value != null
+ && value.isNotEmpty
+ && (int.tryParse(value) ?? -1) > 40) {
FocusScope.of(context).nextFocus();
}
},
@@ -174,11 +178,16 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
medicineId != null && systolic == null && diastolic == null &&
pulse == null && notes == null && needlePin == null) return null;
- if (!widget.settings.allowMissingValues && (value == null || value.isEmpty || int.tryParse(value) == null)) {
+ if (!widget.settings.allowMissingValues
+ && (value == null
+ || value.isEmpty
+ || int.tryParse(value) == null)) {
return localizations.errNaN;
- } else if (widget.settings.validateInputs && (int.tryParse(value ?? '') ?? -1) <= 30) {
+ } else if (widget.settings.validateInputs
+ && (int.tryParse(value ?? '') ?? -1) <= 30) {
return localizations.errLt30;
- } else if (widget.settings.validateInputs && (int.tryParse(value ?? '') ?? 0) >= 400) {
+ } else if (widget.settings.validateInputs
+ && (int.tryParse(value ?? '') ?? 0) >= 400) {
// https://pubmed.ncbi.nlm.nih.gov/7741618/
return localizations.errUnrealistic;
}
@@ -191,8 +200,10 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
/// Build the border all fields have.
- RoundedRectangleBorder buildShapeBorder([Color? color]) => RoundedRectangleBorder(
- side: Theme.of(context).inputDecorationTheme.border?.borderSide ?? const BorderSide(width: 3),
+ RoundedRectangleBorder buildShapeBorder([Color? color]) =>
+ RoundedRectangleBorder(
+ side: Theme.of(context).inputDecorationTheme.border?.borderSide
+ ?? const BorderSide(width: 3),
borderRadius: BorderRadius.circular(20),
);
@@ -204,17 +215,21 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
if (formKey.currentState?.validate() ?? false) {
formKey.currentState?.save();
MedicineIntake? intake;
- if (_showMedicineDosisInput && medicineDosis != null && medicineId != null) {
+ if (_showMedicineDosisInput
+ && medicineDosis != null
+ && medicineId != null) {
intake = MedicineIntake(
timestamp: time,
- medicine: widget.settings.medications.where((e) => e.id == medicineId).first,
+ medicine: widget.settings.medications
+ .where((e) => e.id == medicineId).first,
dosis: medicineDosis!,
);
}
BloodPressureRecord? record;
if (systolic != null || diastolic != null || pulse != null
|| (notes ?? '').isNotEmpty || needlePin != null) {
- record = BloodPressureRecord(time, systolic, diastolic, pulse, notes ?? '', needlePin: needlePin);
+ record = BloodPressureRecord(time, systolic, diastolic, pulse,
+ notes ?? '', needlePin: needlePin,);
}
Navigator.of(context).pop((record, intake));
@@ -379,4 +394,4 @@ Future<(BloodPressureRecord?, MedicineIntake?)?> showAddEntryDialoge(
initialRecord: initialRecord,
),
),
- );
\ No newline at end of file
+ );
lib/components/dialoges/add_medication_dialoge.dart
@@ -6,11 +6,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+/// Dialoge to enter values for a [Medicine].
class AddMedicationDialoge extends StatefulWidget {
+ /// Create a dialoge to enter values for a [Medicine].
const AddMedicationDialoge({super.key,
required this.settings,
});
+ /// Settings that determine general behavior.
final Settings settings;
@override
@@ -102,4 +105,4 @@ class _AddMedicationDialogeState extends State<AddMedicationDialoge> {
Future<Medicine?> showAddMedicineDialoge(BuildContext context, Settings settings) =>
showDialog<Medicine?>(context: context, builder: (context) => Dialog.fullscreen(
child: AddMedicationDialoge(settings: settings),
- ),);
\ No newline at end of file
+ ),);
lib/components/dialoges/enter_timeformat_dialoge.dart
@@ -22,6 +22,8 @@ class EnterTimeFormatDialoge extends StatefulWidget {
/// When previewTime is null [DateTime.now] will be used.
final DateTime? previewTime;
+ /// Whether to move the app bar for saving and loading to the bottom of the
+ /// screen.
final bool bottomAppBars;
@override
@@ -96,4 +98,4 @@ class _EnterTimeFormatDialogeState extends State<EnterTimeFormatDialoge> {
Future<String?> showTimeFormatPickerDialoge(BuildContext context, String initialTimeFormat, bool bottomAppBars) =>
showDialog<String?>(context: context, builder: (context) => Dialog.fullscreen(
child: EnterTimeFormatDialoge(initialValue: initialTimeFormat, bottomAppBars: bottomAppBars,),
- ),);
\ No newline at end of file
+ ),);
lib/components/dialoges/fullscreen_dialoge.dart
@@ -66,4 +66,4 @@ class FullscreenDialoge extends StatelessWidget {
],
);
-}
\ No newline at end of file
+}
lib/components/dialoges/input_dialoge.dart
@@ -23,6 +23,7 @@ class InputDialoge extends StatefulWidget {
/// Optional input validation and formatting overrides.
final List<TextInputFormatter>? inputFormatters;
+ /// The type of keyboard to use for editing the text.
final TextInputType? keyboardType;
/// Validation function called after submit.
@@ -128,4 +129,4 @@ Future<double?> showNumberInputDialoge(BuildContext context, {String? hintText,
double? value = double.tryParse(result ?? '');
value ??= int.tryParse(result ?? '')?.toDouble();
return value;
-}
\ No newline at end of file
+}
lib/components/measurement_list/intake_list_entry.dart
@@ -54,4 +54,4 @@ class IntakeListEntry extends StatelessWidget {
);
-}
\ No newline at end of file
+}
lib/components/measurement_list/measurement_list.dart
@@ -8,13 +8,18 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
+/// List that renders measurements and medicine intakes.
+///
+/// Contains a headline with information about the meaning of "columns".
class MeasurementList extends StatelessWidget {
+ /// Create a list to display measurements and intakes.
const MeasurementList({super.key,
required this.settings,
required this.records,
required this.intakes,
});
+ /// Settings that determine general behavior.
final Settings settings;
/// Records to display.
@@ -110,4 +115,4 @@ class MeasurementList extends StatelessWidget {
],
);
}
-}
\ No newline at end of file
+}
lib/components/measurement_list/measurement_list_entry.dart
@@ -7,11 +7,15 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
-/// Blood pressure measurement to display in a list.
+/// Display of a blood pressure measurement data.
class MeasurementListRow extends StatelessWidget {
+ /// Create a display of a measurements.
const MeasurementListRow({super.key, required this.record, required this.settings});
+ /// The measurement to display.
final BloodPressureRecord record;
+
+ /// Settings that determine general behavior.
final Settings settings;
@override
@@ -20,7 +24,7 @@ class MeasurementListRow extends StatelessWidget {
final formatter = DateFormat(settings.dateFormatString);
return ExpansionTile(
// Leading color possible
- title: buildRow(formatter),
+ 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,
@@ -65,7 +69,7 @@ class MeasurementListRow extends StatelessWidget {
);
}
- Row buildRow(DateFormat formatter) {
+ Row _buildRow(DateFormat formatter) {
String formatNum(int? num) => (num ?? '-').toString();
return Row(
children: [
@@ -108,7 +112,9 @@ class MeasurementListRow extends StatelessWidget {
}
}
-Future<bool> showConfirmDeletionDialoge(BuildContext context) async => await showDialog<bool>(context: context,
+/// Show a dialoge that prompts the user to confirm a deletion.
+Future<bool> showConfirmDeletionDialoge(BuildContext context) async =>
+ await showDialog<bool>(context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.confirmDelete),
content: Text(AppLocalizations.of(context)!.confirmDeleteDesc),
lib/components/settings/color_picker_list_tile.dart
@@ -43,4 +43,4 @@ class ColorSelectionListTile extends StatelessWidget {
if (color != null) onMainColorChanged(color);
},
);
-}
\ No newline at end of file
+}
lib/components/settings/dropdown_list_tile.dart
@@ -60,4 +60,4 @@ class _DropDownListTileState<T> extends State<DropDownListTile<T>> {
onChanged: widget.onChanged,
),
);
-}
\ No newline at end of file
+}
lib/components/settings/input_list_tile.dart
@@ -31,4 +31,4 @@ class InputListTile extends StatelessWidget {
},
);
-}
\ No newline at end of file
+}
lib/components/settings/number_input_list_tile.dart
@@ -39,4 +39,4 @@ class NumberInputListTile extends StatelessWidget {
if (result != null) onParsableSubmit(result);
},
);
-}
\ No newline at end of file
+}
lib/components/settings/settings_widgets.dart
@@ -6,7 +6,7 @@
/// List tiles from flutter and this library currently used by the app are:
/// - [ListTile]
/// - [SwitchListTile]
-/// - [ColorPickerListTile]
+/// - [ColorSelectionListTile]
/// - [DropDownListTile]
/// - [SliderListTile]
/// - [InputListTile]
lib/components/settings/slider_list_tile.dart
@@ -69,4 +69,4 @@ class SliderListTile extends StatelessWidget {
),
);
-}
\ No newline at end of file
+}
lib/components/settings/titled_column.dart
@@ -32,4 +32,4 @@ class TitledColumn extends StatelessWidget {
children: items,
);
}
-}
\ No newline at end of file
+}
lib/components/color_picker.dart
@@ -21,8 +21,8 @@ class ColorPicker extends StatefulWidget {
/// Color that starts out highlighted.
///
- /// When [initialColor] is null the transparent color is selected. When [showTransparentColor] is false as well no
- /// color is selected.
+ /// When [initialColor] is null the transparent color is selected. When
+ /// [showTransparentColor] is false as well no color is selected.
final Color? initialColor;
/// Called after a click on a color.
@@ -103,13 +103,18 @@ class _ColorPickerState extends State<ColorPicker> {
},
child: Container(
decoration: BoxDecoration(
- color: _selected == color ? Theme.of(context).disabledColor : Colors.transparent,
+ color: _selected == color
+ ? Theme.of(context).disabledColor
+ : Colors.transparent,
shape: BoxShape.circle,),
padding: const EdgeInsets.all(5),
child: Container(
height: widget.circleSize,
width: widget.circleSize,
- decoration: BoxDecoration(color: color, shape: BoxShape.circle,),
+ decoration: BoxDecoration(
+ color: color,
+ shape: BoxShape.circle,
+ ),
),
),
),
@@ -124,7 +129,9 @@ class _ColorPickerState extends State<ColorPicker> {
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
- color: _selected == Colors.transparent ? Theme.of(context).disabledColor : Colors.transparent,
+ color: _selected == Colors.transparent
+ ? Theme.of(context).disabledColor
+ : Colors.transparent,
shape: BoxShape.circle,),
child: SizedBox(
height: widget.circleSize,
@@ -140,21 +147,24 @@ class _ColorPickerState extends State<ColorPicker> {
/// Shows a dialog with a ColorPicker and with an cancel button inside.
///
/// Returns the selected color or null when cancel is pressed.
-Future<Color?> showColorPickerDialog(BuildContext context, [Color? initialColor]) async => await showDialog(
- context: context,
- builder: (_) => AlertDialog(
- contentPadding: const EdgeInsets.all(6.0),
- content: ColorPicker(
- initialColor: initialColor,
- onColorSelected: (color) {
- Navigator.of(context).pop(color);
- },
- ),
- actions: [
- TextButton(
- onPressed: Navigator.of(context).pop,
- child: Text(AppLocalizations.of(context)!.btnCancel),
- ),
- ],
+Future<Color?> showColorPickerDialog(
+ BuildContext context, [
+ Color? initialColor,
+]) async => showDialog(
+ context: context,
+ builder: (_) => AlertDialog(
+ contentPadding: const EdgeInsets.all(6.0),
+ content: ColorPicker(
+ initialColor: initialColor,
+ onColorSelected: (color) {
+ Navigator.of(context).pop(color);
+ },
+ ),
+ actions: [
+ TextButton(
+ onPressed: Navigator.of(context).pop,
+ child: Text(AppLocalizations.of(context)!.btnCancel),
),
- );
\ No newline at end of file
+ ],
+ ),
+);
lib/components/consistent_future_builder.dart
@@ -1,37 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+/// A future builder with app defaults.
+///
+/// This allows to have the same loading style everywhere in the app.
class ConsistentFutureBuilder<T> extends StatefulWidget {
+ /// Create a future builder with app defaults.
+ const ConsistentFutureBuilder({
+ super.key,
+ required this.future,
+ this.onNotStarted,
+ this.onWaiting,
+ this.onError,
+ required this.onData,
+ this.cacheFuture = false,
+ this.lastChildWhileWaiting = false,
+ });
- const ConsistentFutureBuilder({super.key, required this.future, this.onNotStarted, this.onWaiting, this.onError,
- required this.onData, this.cacheFuture = false, this.lastChildWhileWaiting = false,});
/// Future that gets evaluated.
final Future<T> future;
+
+ /// The build strategy once the future loaded.
final Widget Function(BuildContext context, T result) onData;
-
+
+ /// The text displayed when no future is connected.
+ ///
+ /// This case should generally be avoided and is protected by assertions in
+ /// debug builds.
+ ///
+ /// Is a 'not started' text by default.
final Widget? onNotStarted;
+
+ /// The future loading indicator.
+ ///
+ /// Shown while the element is loading. Defaults to 'loading... text'.
final Widget? onWaiting;
+
+ /// The build strategy in case the future throws an error.
+ ///
+ /// Shows the error message by default.
+ /// FIXME: Currently ignored
final Widget? Function(BuildContext context, String errorMsg)? onError;
/// Internally save the future and avoid rebuilds.
///
- /// Caching will allow the future builder not to load again in some cases where a rebuild is triggered. But it comes at
- /// the cost that onData will not be called again, even if data changed.
+ /// Caching will allow the future builder not to load again in some cases
+ /// where a rebuild is triggered. But it comes at the cost that onData will
+ /// not be called again, even if data changed.
///
- /// The parameter is false by default and should only be set to true when rebuilds are disruptive to the user and it
- /// is certain that the data will not change over the lifetime of the screen.
+ /// The parameter is false by default and should only be set to true when
+ /// rebuilds are disruptive to the user and it is certain that the data will
+ /// not change over the lifetime of the screen.
final bool cacheFuture;
- /// When loading the next result the child that got build the last time will be returned.
+ /// When loading the next result the child that got build the last time will
+ /// be returned.
///
/// During the first build, [onWaiting] os respected instead.
final bool lastChildWhileWaiting;
@override
- State<ConsistentFutureBuilder<T>> createState() => _ConsistentFutureBuilderState<T>();
+ State<ConsistentFutureBuilder<T>> createState() =>
+ _ConsistentFutureBuilderState<T>();
}
-class _ConsistentFutureBuilderState<T> extends State<ConsistentFutureBuilder<T>> {
+class _ConsistentFutureBuilderState<T>
+ extends State<ConsistentFutureBuilder<T>> {
Future<T>? _future; // avoid rebuilds
/// Used for returning the last child during load when rebuilding.
Widget? _lastChild;
@@ -46,21 +80,26 @@ class _ConsistentFutureBuilderState<T> extends State<ConsistentFutureBuilder<T>>
@override
Widget build(BuildContext context) => FutureBuilder<T>(
- future: _future ?? widget.future,
- builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
- if (snapshot.hasError) {
- return Text(AppLocalizations.of(context)?.error(snapshot.error.toString()) ?? snapshot.error.toString());
- }
- switch (snapshot.connectionState) {
- case ConnectionState.none:
- return widget.onNotStarted ?? Text(AppLocalizations.of(context)!.errNotStarted);
- case ConnectionState.waiting:
- case ConnectionState.active:
- if (widget.lastChildWhileWaiting && _lastChild != null) return _lastChild!;
- return widget.onWaiting ?? Text(AppLocalizations.of(context)!.loading);
- case ConnectionState.done:
- _lastChild = widget.onData(context, snapshot.data as T);
+ future: _future ?? widget.future,
+ builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
+ final localizations = AppLocalizations.of(context)!;
+ if (snapshot.hasError) {
+ return Text(localizations.error(snapshot.error.toString()));
+ }
+ switch (snapshot.connectionState) {
+ case ConnectionState.none:
+ assert(false);
+ return widget.onNotStarted ?? Text(localizations.errNotStarted);
+ case ConnectionState.waiting:
+ case ConnectionState.active:
+ if (widget.lastChildWhileWaiting && _lastChild != null) {
return _lastChild!;
- }
- },);
-}
\ No newline at end of file
+ }
+ return widget.onWaiting ?? Text(localizations.loading);
+ case ConnectionState.done:
+ _lastChild = widget.onData(context, snapshot.data as T);
+ return _lastChild!;
+ }
+ },
+ );
+}
lib/components/date_time_picker.dart
@@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-/// First shows a DatePicker for the day then shows a TimePicker for the time of day.
+/// First shows a DatePicker for the day then shows a TimePicker for the time of
+/// day.
///
-/// As per the decision of the material design team a TimePicker isn't able to limit the range
-/// (https://github.com/flutter/flutter/issues/23717#issuecomment-966601311), therefore a manual check for the time of
-/// day will be needed. Refer to the validator on the AddMeasurementPage for an example
+/// As per the decision of the material design team a TimePicker isn't able to
+/// limit the range (https://github.com/flutter/flutter/issues/23717#issuecomment-966601311),
+/// therefore a manual check for the time of day will be needed. Refer to the
+/// validator on the AddMeasurementPage for an example.
Future<DateTime?> showDateTimePicker({
required BuildContext context,
DateTime? initialDate,
@@ -17,7 +19,12 @@ Future<DateTime?> showDateTimePicker({
lastDate ??= firstDate.add(const Duration(days: 365 * 200));
final DateTime? selectedDate = await showDatePicker(
- context: context, initialDate: initialDate, firstDate: firstDate, lastDate: lastDate, confirmText: AppLocalizations.of(context)!.btnNext,);
+ context: context,
+ initialDate: initialDate,
+ firstDate: firstDate,
+ lastDate: lastDate,
+ confirmText: AppLocalizations.of(context)!.btnNext,
+ );
if (selectedDate == null) return null;
if (!context.mounted) return null;
lib/components/disabled.dart
@@ -1,17 +1,22 @@
import 'package:flutter/material.dart';
-/// A widget that visually indicates that it's subtree is disabled and blocks all interaction with it.
+/// A widget that visually indicates that it's subtree is disabled and blocks
+/// all interaction with it.
class Disabled extends StatelessWidget {
- /// Create a widget that visually indicates that it's subtree is disabled and blocks interaction with it.
+ /// Create a widget that visually indicates that it's subtree is disabled
+ /// and blocks interaction with it.
///
- /// If [disabled] is true the [child]s opacity gets reduced and interaction gets disabled. This widget has no effect
- /// when [disabled] is false.
+ /// If [disabled] is true the [child]s opacity gets reduced and interaction
+ /// gets disabled. This widget has no effect when [disabled] is false.
const Disabled({required this.child, this.disabled = true, this.ignoring = true, super.key});
+ /// The widget to render as disabled.
final Widget child;
+
/// Whether this widget has an effect.
final bool disabled;
+
/// Whether interaction is blocked.
final bool ignoring;
@@ -29,4 +34,4 @@ class Disabled extends StatelessWidget {
return child;
}
-}
\ No newline at end of file
+}
lib/model/blood_pressure/medicine/intake_history.dart
@@ -137,4 +137,4 @@ class IntakeHistory extends ChangeNotifier {
@override
int get hashCode => _medicineIntakes.hashCode;
-}
\ No newline at end of file
+}
lib/model/blood_pressure/medicine/medicine.dart
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
/// Description of a specific medicine.
class Medicine {
-
+ /// Create a instance from a map created by [toMap].
factory Medicine.fromMap(Map<String, dynamic> map) => Medicine(
map['id'],
designation: map['designation'],
@@ -14,7 +14,9 @@ class Medicine {
defaultDosis: map['defaultDosis'],
);
+ /// Create a instance from a [String] created by [toJson].
factory Medicine.fromJson(String json) => Medicine.fromMap(jsonDecode(json));
+
/// Create a new medicine.
const Medicine(this.id, {
required this.designation,
@@ -22,6 +24,7 @@ class Medicine {
required this.defaultDosis,
});
+ /// Serialize the object to a restoreable map.
Map<String, dynamic> toMap() => {
'id': id,
'designation': designation,
@@ -29,6 +32,7 @@ class Medicine {
'defaultDosis': defaultDosis,
};
+ /// Serialize the object to a restoreable string.
String toJson() => jsonEncode(toMap());
/// Unique id used to store the medicine in serialized objects.
lib/model/blood_pressure/medicine/medicine_intake.dart
@@ -3,7 +3,7 @@ import 'package:blood_pressure_app/model/blood_pressure/medicine/medicine.dart';
/// Instance of a medicine intake.
class MedicineIntake implements Comparable<Object> {
-
+ /// Create a intake from a String created by [serialize].
factory MedicineIntake.deserialize(String string, List<Medicine> availableMeds) {
final elements = string.split('\x00');
return MedicineIntake(
@@ -12,6 +12,7 @@ class MedicineIntake implements Comparable<Object> {
dosis: double.parse(elements[2]),
);
}
+
/// Create a instance of a medicine intake.
const MedicineIntake({
required this.medicine,
@@ -60,4 +61,4 @@ class MedicineIntake implements Comparable<Object> {
@override
String toString() => 'MedicineIntake{medicine: $medicine, dosis: $dosis, timestamp: $timestamp}';
-}
\ No newline at end of file
+}
lib/model/blood_pressure/model.dart
@@ -12,10 +12,13 @@ import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:sqflite/sqflite.dart';
+/// Model to access values in the measurement database.
class BloodPressureModel extends ChangeNotifier {
BloodPressureModel._create();
+
late final Database _database;
+
Future<void> _asyncInit(String? dbPath, bool isFullPath) async {
dbPath ??= await getDatabasesPath();
@@ -117,6 +120,10 @@ class BloodPressureModel extends ChangeNotifier {
}
}
+ /// Try to remove the measurement at a specific timestamp from the database.
+ ///
+ /// When no measurement at that time exists, the operation won't fail and
+ /// listeners will get notified anyways.
Future<void> delete(DateTime timestamp) async {
if (!_database.isOpen) return;
_database.delete('bloodPressureModel', where: 'timestamp = ?', whereArgs: [timestamp.millisecondsSinceEpoch]);
@@ -134,11 +141,15 @@ class BloodPressureModel extends ChangeNotifier {
return UnmodifiableListView(recordsInRange);
}
+ /// Querries all measurements saved in the database.
Future<UnmodifiableListView<BloodPressureRecord>> get all async {
if (!_database.isOpen) return UnmodifiableListView([]);
return UnmodifiableListView(_convert(await _database.query('bloodPressureModel', columns: ['*'])));
}
+ /// Close the database.
+ ///
+ /// Cannot be accessed anymore.
Future<void> close() => _database.close();
List<BloodPressureRecord> _convert(List<Map<String, Object?>> dbResult) {
@@ -158,4 +169,3 @@ class BloodPressureModel extends ChangeNotifier {
return records;
}
}
-
lib/model/blood_pressure/needle_pin.dart
@@ -1,17 +1,26 @@
+import 'package:blood_pressure_app/model/blood_pressure/record.dart';
import 'package:flutter/material.dart';
@immutable
+/// Metadata and secondary information for a [BloodPressureRecord].
class MeasurementNeedlePin {
-
+ /// Create metadata for a [BloodPressureRecord].
const MeasurementNeedlePin(this.color);
- // When updating this, remember to be backwards compatible
+
+ /// Create a instance from a map created by [toMap].
MeasurementNeedlePin.fromJson(Map<String, dynamic> json)
: color = Color(json['color']);
+ // When updating this, remember to be backwards compatible.
+ // (or reimplement the system)
+
+ /// The color associated with the measurement.
final Color color;
- Map<String, dynamic> toJson() => {
+
+ /// Serialize the object to a restoreable map.
+ Map<String, dynamic> toMap() => {
'color': color.value,
};
@override
String toString() => 'MeasurementNeedlePin{$color}';
-}
\ No newline at end of file
+}
lib/model/blood_pressure/record.dart
@@ -2,8 +2,9 @@ import 'package:blood_pressure_app/model/blood_pressure/needle_pin.dart';
import 'package:flutter/material.dart';
@immutable
+/// Immutable data representation of a saved measurement.
class BloodPressureRecord {
-
+ /// Create a measurement.
BloodPressureRecord(DateTime creationTime, this.systolic, this.diastolic, this.pulse, this.notes, {
this.needlePin,
}) {
@@ -14,13 +15,25 @@ class BloodPressureRecord {
this.creationTime = DateTime.fromMillisecondsSinceEpoch(1);
}
}
+
+ /// The time the measurement was created.
late final DateTime creationTime;
+
+ /// The stored sys value.
final int? systolic;
+
+ /// The stored dia value.
final int? diastolic;
+
+ /// The stored pul value.
final int? pulse;
+
+ /// Notes stored about this measurement.
final String notes;
+
+ /// Secondary information about the measurement.
final MeasurementNeedlePin? needlePin;
@override
String toString() => 'BloodPressureRecord($creationTime, $systolic, $diastolic, $pulse, $notes, $needlePin)';
-}
\ No newline at end of file
+}
lib/model/blood_pressure/warn_values.dart
@@ -1,9 +1,13 @@
-// source: https://pressbooks.library.torontomu.ca/vitalsign/chapter/blood-pressure-ranges/ (last access: 14.11.2023)
+/// Static provider of warn values for ages.
+///
+/// source: https://pressbooks.library.torontomu.ca/vitalsign/chapter/blood-pressure-ranges/ (last access: 14.11.2023)
class BloodPressureWarnValues {
BloodPressureWarnValues._create();
+ /// URL from which the information was taken.
static String source = 'https://pressbooks.library.torontomu.ca/vitalsign/chapter/blood-pressure-ranges/';
+ /// Returns the default highest (safe) diastolic value for a specific age.
static int getUpperDiaWarnValue(int age) {
if (age <= 2) {
return 70;
@@ -20,6 +24,7 @@ class BloodPressureWarnValues {
}
}
+ /// Returns the default highest (safe) systolic value for a specific age.
static int getUpperSysWarnValue(int age) {
if (age <= 2) {
return 100;
@@ -35,4 +40,4 @@ class BloodPressureWarnValues {
return 145;
}
}
-}
\ No newline at end of file
+}
lib/model/export_import/column.dart
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:blood_pressure_app/model/blood_pressure/needle_pin.dart';
import 'package:blood_pressure_app/model/blood_pressure/record.dart';
+import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
import 'package:blood_pressure_app/model/export_import/import_field_type.dart';
import 'package:blood_pressure_app/model/export_import/record_formatter.dart';
import 'package:flutter/material.dart';
@@ -69,7 +70,7 @@ class NativeColumn extends ExportColumn {
static final NativeColumn needlePin = NativeColumn._create(
'needlePin',
RowDataFieldType.needlePin,
- (record) => jsonEncode(record.needlePin?.toJson()),
+ (record) => jsonEncode(record.needlePin?.toMap()),
(pattern) {
final json = jsonDecode(pattern);
if (json is! Map<String, dynamic>) return null;
@@ -267,6 +268,8 @@ class UserColumn extends ExportColumn {
RowDataFieldType? get restoreAbleType => formatter.restoreAbleType;
}
+/// A measurement formatters that converts the timestamp to a string using ICU
+/// patterns.
class TimeColumn extends ExportColumn {
/// Create a formatter that converts between [String]s and [DateTime]s
/// through a format pattern.
lib/model/export_import/export_configuration.dart
@@ -19,6 +19,7 @@ class ActiveExportColumnConfiguration extends ChangeNotifier {
_activePreset = activePreset ?? ExportImportPreset.bloodPressureApp,
_userSelectedColumns = userSelectedColumnIds ?? [];
+ /// Create a instance from a [String] created by [toJson].
factory ActiveExportColumnConfiguration.fromJson(String jsonString) {
try {
final json = jsonDecode(jsonString);
@@ -32,6 +33,7 @@ class ActiveExportColumnConfiguration extends ChangeNotifier {
}
+ /// Serialize the object to a restoreable string.
String toJson() => jsonEncode({
'columns': _userSelectedColumns,
'preset': _activePreset.encode(),
@@ -43,7 +45,10 @@ class ActiveExportColumnConfiguration extends ChangeNotifier {
final List<String> _userSelectedColumns;
ExportImportPreset _activePreset;
+
+ /// The current selection on what set of values will be exported.
ExportImportPreset get activePreset => _activePreset;
+
set activePreset(ExportImportPreset value) {
_activePreset = value;
notifyListeners();
@@ -124,8 +129,10 @@ enum ExportImportPreset {
/// Includes formatted time, sys, dia and pulse.
bloodPressureAppPdf,
+ /// Preset for exporting data to the myHeart app.
myHeart;
+ /// Selection of a displayable string from [localizations].
String localize(AppLocalizations localizations) => switch (this) {
ExportImportPreset.none => localizations.custom,
ExportImportPreset.bloodPressureApp => localizations.default_,
@@ -133,6 +140,7 @@ enum ExportImportPreset {
ExportImportPreset.myHeart => '"My Heart" export'
};
+ /// Turn the value into a [decode]able integer for serialization purposes.
int encode() => switch (this) {
ExportImportPreset.none => 0,
ExportImportPreset.bloodPressureApp => 1,
@@ -140,7 +148,8 @@ enum ExportImportPreset {
ExportImportPreset.bloodPressureAppPdf => 3,
};
- static ExportImportPreset? decode(e) => switch(e) {
+ /// Create a enum value form a number returned by [encode].
+ static ExportImportPreset? decode(Object? e) => switch(e) {
0 => ExportImportPreset.none,
1 => ExportImportPreset.bloodPressureApp,
2 => ExportImportPreset.myHeart,
@@ -150,4 +159,4 @@ enum ExportImportPreset {
return null;
}(),
};
-}
\ No newline at end of file
+}
lib/model/export_import/import_field_type.dart
@@ -4,8 +4,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Type a [Formatter] can uses to indicate the kind of data returned.
-///
-/// The data types returned from the deprecated [LegacyExportColumn] may differ from the guarantees.
enum RowDataFieldType {
/// Guarantees [DateTime] is returned.
timestamp,
@@ -20,6 +18,7 @@ enum RowDataFieldType {
/// Guarantees that the returned type is of type [MeasurementNeedlePin].
needlePin;
+ /// Selection of a displayable string from [localizations].
String localize(AppLocalizations localizations) {
switch(this) {
case RowDataFieldType.timestamp:
@@ -36,4 +35,4 @@ enum RowDataFieldType {
return localizations.color;
}
}
-}
\ No newline at end of file
+}
lib/model/export_import/pdf_converter.dart
@@ -57,7 +57,7 @@ class PdfConverter {
},
maxPages: 100,
),);
- return await pdf.save();
+ return pdf.save();
}
pw.Widget _buildPdfTitle(List<BloodPressureRecord> records, BloodPressureAnalyser analyzer) {
@@ -191,8 +191,6 @@ class PdfConverter {
int rowCount = (availableHeightOnFirstPage - realHeaderHeight)
~/ (realCellHeight);
-
- print(rowCount);
final List<pw.Widget> tables = [];
int pageNum = 0;
@@ -263,6 +261,6 @@ class PdfConverter {
}
}
-extension PdfCompatability on Color {
+extension _PdfCompatability on Color {
PdfColor toPdfColor() => PdfColor(red / 256, green / 256, blue / 256, opacity);
-}
\ No newline at end of file
+}
lib/model/export_import/record_formatter.dart
@@ -9,7 +9,7 @@ import 'package:intl/intl.dart';
/// Class to serialize and deserialize [BloodPressureRecord] values.
abstract interface class Formatter {
- /// Pattern that a user can use to achieve the effect of [convertToCsvValue].
+ /// Pattern that a user can use to achieve the effect of [encode].
String? get formatPattern;
/// Creates a string representation of the record.
@@ -31,8 +31,9 @@ abstract interface class Formatter {
(RowDataFieldType, dynamic)? decode(String pattern);
}
-/// Record [Formatter] that is based on a format pattern.
+/// Measurement [Formatter] that is based on a format pattern.
class ScriptedFormatter implements Formatter {
+ /// Create a [BloodPressureRecord] formatter from a format pattern.
ScriptedFormatter(this.pattern);
/// Pattern used for formatting values.
@@ -79,7 +80,7 @@ class ScriptedFormatter implements Formatter {
fieldContents = fieldContents.replaceAll(r'$DIA', record.diastolic.toString());
fieldContents = fieldContents.replaceAll(r'$PUL', record.pulse.toString());
fieldContents = fieldContents.replaceAll(r'$NOTE', record.notes);
- fieldContents = fieldContents.replaceAll(r'$COLOR', jsonEncode(record.needlePin?.toJson()));
+ fieldContents = fieldContents.replaceAll(r'$COLOR', jsonEncode(record.needlePin?.toMap()));
// math
fieldContents = fieldContents.replaceAllMapped(RegExp(r'\{\{([^}]*)}}'), (m) {
@@ -177,26 +178,26 @@ class ScriptedTimeFormatter implements Formatter {
///
/// The pattern follows the ICU style like [DateFormat].
ScriptedTimeFormatter(String newPattern):
- timeFormatter = DateFormat(newPattern);
+ _timeFormatter = DateFormat(newPattern);
- final DateFormat timeFormatter;
+ final DateFormat _timeFormatter;
@override
(RowDataFieldType, dynamic)? decode(String pattern) {
if (pattern.isEmpty) return null;
try {
- return (RowDataFieldType.timestamp, timeFormatter.parseLoose(pattern));
+ return (RowDataFieldType.timestamp, _timeFormatter.parseLoose(pattern));
} on FormatException {
return null;
}
}
@override
- String encode(BloodPressureRecord record) => timeFormatter.format(record.creationTime);
+ String encode(BloodPressureRecord record) => _timeFormatter.format(record.creationTime);
@override
- String? get formatPattern => timeFormatter.pattern;
+ String? get formatPattern => _timeFormatter.pattern;
@override
RowDataFieldType? get restoreAbleType => RowDataFieldType.timestamp;
-}
\ No newline at end of file
+}
lib/model/export_import/record_parsing_result.dart
@@ -68,4 +68,4 @@ class RecordParsingErrorUnparsableField implements RecordParsingError {
/// Text in the csv string that failed to parse.
final String fieldContents;
-}
\ No newline at end of file
+}
lib/model/storage/db/config_dao.dart
@@ -9,12 +9,15 @@ import 'package:sqflite/sqflite.dart';
/// Class for loading data from the database.
///
-/// The user of this class needs to pay attention to dispose all old instances of objects created by the instance
-/// methods in order to ensure there are no concurrent writes to the database. Having multiple instances will cause data
+/// The user of this class needs to pay attention to dispose all old instances
+/// of objects created by the instance methods in order to ensure there are no
+/// concurrent writes to the database. Having multiple instances will cause data
/// loss because states are not synced again after one changes.
///
-/// The load... methods have to schedule a initial save to db in case an migration / update of fields occurred.
+/// The load... methods have to schedule a initial save to db in case an
+/// migration / update of fields occurred.
class ConfigDao {
+ /// Create a serializer to initialize data from a database.
ConfigDao(this._configDB);
final ConfigDB _configDB;
@@ -22,8 +25,9 @@ class ConfigDao {
final Map<int, Settings> _settingsInstances = {};
/// Loads the profiles [Settings] object from the database.
///
- /// If any errors occur or the object is not present, a default one will be created. Changes in the object
- /// will save to the database automatically (a listener gets attached).
+ /// If any errors occur or the object is not present, a default one will be
+ /// created. Changes in the object will save to the database automatically (a
+ /// listener gets attached).
///
/// Changes to the database will not propagate to the object.
Future<Settings> loadSettings(int profileID) async {
@@ -330,4 +334,4 @@ class ConfigDao {
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
-}
\ No newline at end of file
+}
lib/model/storage/db/config_db.dart
@@ -72,7 +72,7 @@ class ConfigDB {
'INTEGER, storage_id INTEGER, stepSize INTEGER, start INTEGER, end INTEGER, '
'PRIMARY KEY(profile_id, storage_id))';
- /// Name of the exportStrings table. It is used to store formats used in the [ExportConfigurationModel].
+ /// Name of the exportStrings table. It is used to to update old columns.
///
/// Format:
/// `CREATE TABLE exportStrings(internalColumnName STRING PRIMARY KEY, columnTitle STRING, formatPattern STRING)`
lib/model/storage/common_settings_interfaces.dart
@@ -8,4 +8,4 @@ abstract class CustomFieldsSettings {
///
/// Implementers must propagate any notifyListener calls.
ActiveExportColumnConfiguration get exportFieldsConfiguration;
-}
\ No newline at end of file
+}
lib/model/storage/convert_util.dart
@@ -90,7 +90,8 @@ class ConvertUtil {
return null;
}
- static ThemeMode? parseThemeMode(value) {
+ /// Try to recreate the theme mode stored as a integer.
+ static ThemeMode? parseThemeMode(dynamic value) {
final int? intValue = ConvertUtil.parseInt(value);
switch(intValue) {
case null:
@@ -106,4 +107,4 @@ class ConvertUtil {
return null;
}
}
-}
\ No newline at end of file
+}
lib/model/storage/export_columns_store.dart
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
/// Class for managing columns available to the user.
class ExportColumnsManager extends ChangeNotifier {
-
+ /// Create a instance from a map created by [toMap].
factory ExportColumnsManager.fromMap(Map<String, dynamic> map) {
final List<dynamic> jsonUserColumns = map['userColumns'];
final manager = ExportColumnsManager();
@@ -25,6 +25,7 @@ class ExportColumnsManager extends ChangeNotifier {
return manager;
}
+ /// Create a instance from a [String] created by [toJson].
factory ExportColumnsManager.fromJson(String jsonString) {
try {
return ExportColumnsManager.fromMap(jsonDecode(jsonString));
@@ -38,15 +39,19 @@ class ExportColumnsManager extends ChangeNotifier {
/// It will be filled with the default columns but won't contain initial user columns.
ExportColumnsManager();
+ /// Namespaces that may not lead a user columns internal identifier.
static const List<String> reservedNamespaces = ['buildIn', 'myHeart'];
- /// Map between all [ExportColumn.internalIdentifier]s and [ExportColumn]s added by a user.
+ /// Map between all [ExportColumn.internalIdentifier]s and [ExportColumn]s
+ /// added by a user.
final Map<String, ExportColumn> _userColumns = {};
- /// View of map between all [ExportColumn.internalName]s and [ExportColumn]s added by a user.
+ /// View of map between all [ExportColumn.internalIdentifier]s and columns
+ /// added by a user.
UnmodifiableMapView<String, ExportColumn> get userColumns => UnmodifiableMapView(_userColumns);
- /// Tries to save the column to the map with the [ExportColumn.internalName] key.
+ /// Tries to save the column to the map with the [ExportColumn.internalIdentifier]
+ /// key.
///
/// This method fails and returns false when there is a default [ExportColumn] with the same internal name is
/// available.
@@ -77,12 +82,11 @@ class ExportColumnsManager extends ChangeNotifier {
/// 1. userColumns
/// 2. NativeColumn
/// 3. BuildInColumn
- ExportColumn? firstWhere(bool Function(ExportColumn) test) {
- return userColumns.values.where(test).firstOrNull
- ?? NativeColumn.allColumns.where(test).firstOrNull
- ?? BuildInColumn.allColumns.where(test).firstOrNull;
- // ?? ...
- }
+ ExportColumn? firstWhere(bool Function(ExportColumn) test) =>
+ userColumns.values.where(test).firstOrNull
+ ?? NativeColumn.allColumns.where(test).firstOrNull
+ ?? BuildInColumn.allColumns.where(test).firstOrNull;
+ // ?? ...
/// Returns a list of all userColumns, NativeColumns and BuildInColumns defined.
///
@@ -103,6 +107,7 @@ class ExportColumnsManager extends ChangeNotifier {
return UnmodifiableListView(columns);
}
+ /// Serialize the object to a restoreable map.
Map<String, dynamic> toMap() {
final columns = [];
for (final c in _userColumns.values) {
@@ -134,6 +139,7 @@ class ExportColumnsManager extends ChangeNotifier {
};
}
+ /// Serialize the object to a restoreable string.
String toJson() => jsonEncode(toMap());
}
lib/model/storage/intervall_store.dart
@@ -7,12 +7,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Class for storing the current interval, as it is needed in start page, statistics and export.
class IntervallStorage extends ChangeNotifier {
-
+ /// Create a instance from a map created by [toMap].
factory IntervallStorage.fromMap(Map<String, dynamic> map) => IntervallStorage(
stepSize: TimeStep.deserialize(map['stepSize']),
range: ConvertUtil.parseRange(map['start'], map['end']),
);
+ /// Create a instance from a [String] created by [toJson].
factory IntervallStorage.fromJson(String json) {
try {
return IntervallStorage.fromMap(jsonDecode(json));
@@ -20,19 +21,25 @@ class IntervallStorage extends ChangeNotifier {
return IntervallStorage();
}
}
- IntervallStorage({TimeStep? stepSize, DateTimeRange? range}) {
- _stepSize = stepSize ?? TimeStep.last7Days;
+
+ /// Create a storage to interact with a display intervall.
+ IntervallStorage({TimeStep? stepSize, DateTimeRange? range}) :
+ _stepSize = stepSize ?? TimeStep.last7Days {
_currentRange = range ?? _getMostRecentDisplayIntervall();
}
- late TimeStep _stepSize;
+
+ TimeStep _stepSize;
+
late DateTimeRange _currentRange;
+ /// Serialize the object to a restoreable map.
Map<String, dynamic> toMap() => <String, dynamic>{
'stepSize': stepSize.serialize(),
'start': currentRange.start.millisecondsSinceEpoch,
'end': currentRange.end.millisecondsSinceEpoch,
};
+ /// Serialize the object to a restoreable string.
String toJson() => jsonEncode(toMap());
/// The stepSize gets set through the changeStepSize method.
@@ -155,6 +162,34 @@ enum TimeStep {
last30Days,
custom;
+ /// Recreate a TimeStep from a number created with [TimeStep.serialize].
+ factory TimeStep.deserialize(value) {
+ final int? intValue = ConvertUtil.parseInt(value);
+ if (intValue == null) return TimeStep.last7Days;
+
+ switch (intValue) {
+ case 0:
+ return TimeStep.day;
+ case 1:
+ return TimeStep.month;
+ case 2:
+ return TimeStep.year;
+ case 3:
+ return TimeStep.lifetime;
+ case 4:
+ return TimeStep.week;
+ case 5:
+ return TimeStep.last7Days;
+ case 6:
+ return TimeStep.last30Days;
+ case 7:
+ return TimeStep.custom;
+ default:
+ assert(false);
+ return TimeStep.last7Days;
+ }
+ }
+
static const options = [TimeStep.day, TimeStep.week, TimeStep.month, TimeStep.year, TimeStep.lifetime, TimeStep.last7Days, TimeStep.last30Days, TimeStep.custom];
String getName(AppLocalizations localizations) {
@@ -198,33 +233,6 @@ enum TimeStep {
return 7;
}
}
-
- factory TimeStep.deserialize(value) {
- final int? intValue = ConvertUtil.parseInt(value);
- if (intValue == null) return TimeStep.last7Days;
-
- switch (intValue) {
- case 0:
- return TimeStep.day;
- case 1:
- return TimeStep.month;
- case 2:
- return TimeStep.year;
- case 3:
- return TimeStep.lifetime;
- case 4:
- return TimeStep.week;
- case 5:
- return TimeStep.last7Days;
- case 6:
- return TimeStep.last30Days;
- case 7:
- return TimeStep.custom;
- default:
- assert(false);
- return TimeStep.last7Days;
- }
- }
}
/// Class that stores the interval objects that are needed in the app and provides named access to them.
lib/model/storage/storage.dart
@@ -22,4 +22,4 @@ export 'export_pdf_settings_store.dart';
export 'export_settings_store.dart';
export 'intervall_store.dart';
export 'settings_store.dart';
-export 'update_legacy_settings.dart';
\ No newline at end of file
+export 'update_legacy_settings.dart';
lib/model/storage/update_legacy_settings.dart
@@ -224,4 +224,4 @@ Future<void> updateLegacyExport(ConfigDB database, ExportColumnsManager manager)
}
Future<bool> _tableExists(Database database, String tableName) async => (await database.rawQuery(
- "SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName';",)).isNotEmpty;
\ No newline at end of file
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName';",)).isNotEmpty;
lib/model/blood_pressure_analyzer.dart
@@ -3,37 +3,51 @@ import 'dart:math';
import 'package:blood_pressure_app/model/blood_pressure/record.dart';
import 'package:collection/collection.dart';
+/// Analysis utils for a list of blood pressure records.
class BloodPressureAnalyser {
-
+ /// Create a analyzer for a list of records.
BloodPressureAnalyser(this._records);
+
final List<BloodPressureRecord> _records;
+ /// The amount of records saved.
int get count => _records.length;
+ /// The average diastolic values of all records.
int get avgDia => _safeResult(() => _nonNullDia.average.toInt(), (r) => r.diastolic);
+ /// The average pulse values of all records.
int get avgPul => _safeResult(() => _nonNullPul.average.toInt(), (r) => r.pulse);
+ /// The average systolic values of all records.
int get avgSys => _safeResult(() => _nonNullSys.average.toInt(), (r) => r.systolic);
+ /// The maximum diastolic values of all records.
int get maxDia => _safeResult(() => _nonNullDia.reduce(max), (r) => r.diastolic);
+ /// The maximum pulse values of all records.
int get maxPul => _safeResult(() => _nonNullPul.reduce(max), (r) => r.pulse);
+ /// The maximum systolic values of all records.
int get maxSys => _safeResult(() => _nonNullSys.reduce(max), (r) => r.systolic);
+ /// The minimal diastolic values of all records.
int get minDia => _safeResult(() => _nonNullDia.reduce(min), (r) => r.diastolic);
+ /// The minimal pulse values of all records.
int get minPul => _safeResult(() => _nonNullPul.reduce(min), (r) => r.pulse);
+ /// The minimal systolic values of all records.
int get minSys => _safeResult(() => _nonNullSys.reduce(min), (r) => r.systolic);
+ /// The earliest timestamp of all records.
DateTime? get firstDay {
if (_records.isEmpty) return null;
_records.sort((a, b) => a.creationTime.compareTo(b.creationTime));
return _records.first.creationTime;
}
+ /// The latest timestamp of all records.
DateTime? get lastDay {
if (_records.isEmpty) return null;
_records.sort((a, b) => a.creationTime.compareTo(b.creationTime));
@@ -54,6 +68,7 @@ class BloodPressureAnalyser {
Iterable<int> get _nonNullSys => _records.map((e) => e.systolic).whereNotNull();
Iterable<int> get _nonNullPul => _records.map((e) => e.pulse).whereNotNull();
+ /// Average amount of measurements entered per day.
int get measurementsPerDay {
final c = count;
if (c <= 1) return -1;
@@ -72,6 +87,8 @@ class BloodPressureAnalyser {
return c ~/ lastDay.difference(firstDay).inDays;
}
+ /// Relation of average values to the time of the day.
+ ///
/// outer list is type (0 -> diastolic, 1 -> systolic, 2 -> pulse)
/// inner list index is hour of day ([0] -> 00:00-00:59; [1] -> ...)
List<List<int>> get allAvgsRelativeToDaytime {
lib/model/horizontal_graph_line.dart
@@ -1,11 +1,17 @@
import 'package:blood_pressure_app/model/blood_pressure/record.dart';
import 'package:flutter/material.dart';
+/// Information about a straight horizontal line through the graph.
+///
+/// Can be used to indicate value ranges and provide a frame of reference to the
+/// user.
class HorizontalGraphLine {
-
+ /// Create a instance from a [String] created by [toJson].
HorizontalGraphLine.fromJson(Map<String, dynamic> json)
: color = Color(json['color']),
height = json['height'];
+
+ /// Create information about a new horizontal line through the graph.
HorizontalGraphLine(this.color, this.height);
/// Color of the line.
@@ -16,8 +22,9 @@ class HorizontalGraphLine {
/// Usually on the same scale as [BloodPressureRecord]
int height;
+ /// Serialize the object to a restoreable string.
Map<String, dynamic> toJson() => {
'color': color.value,
'height': height,
};
-}
\ No newline at end of file
+}
lib/model/iso_lang_names.dart
@@ -185,4 +185,7 @@ final _isoLangs = {
'za': 'Saɯ cueŋƅ, Saw cuengh',
};
+/// Selects the correct language name for a specific language code.
+///
+/// Does not account for dialects.
String? getDisplayLanguage(Locale l) => _isoLangs[l.languageCode];
lib/platform_integration/platform_client.dart
@@ -65,4 +65,4 @@ class PlatformClient {
return false;
}
}
-}
\ No newline at end of file
+}
lib/screens/elements/blood_pressure_builder.dart
@@ -7,11 +7,19 @@ import 'package:blood_pressure_app/model/storage/intervall_store.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-/// Shorthand class for getting the blood pressure values
+/// Shorthand class for getting the blood pressure values.
class BloodPressureBuilder extends StatelessWidget {
- const BloodPressureBuilder({required this.onData, required this.rangeType, super.key});
+ /// Create a loader for the measurements in the current range.
+ const BloodPressureBuilder({
+ super.key,
+ required this.onData,
+ required this.rangeType,
+ });
+ /// The build strategy once the measurement are loaded.
final Widget Function(BuildContext context, UnmodifiableListView<BloodPressureRecord> records) onData;
+
+ /// Which measurements to load.
final IntervallStoreManagerLocation rangeType;
@override
lib/screens/elements/display_interval_picker.dart
@@ -4,9 +4,15 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
+/// A selector for [IntervallStorage] values.
+///
+/// Allows selection [IntervallStorage.currentRange] and moving intervall wise
+/// in both directions.
class IntervalPicker extends StatelessWidget {
+ /// Create a selector for [IntervallStorage] values.
const IntervalPicker({super.key, required this.type});
+ /// Which range to display and modify.
final IntervallStoreManagerLocation type;
@override
lib/screens/elements/legacy_measurement_list.dart
@@ -11,8 +11,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
+/// A old more compact [BloodPressureRecord] list, that lacks some of the new
+/// features.
class LegacyMeasurementsList extends StatelessWidget {
-
+ /// Create a more compact measurement list without all new features.
LegacyMeasurementsList(BuildContext context, {super.key}) {
if (MediaQuery.of(context).size.width < 1000) {
_tableElementsSizes = [33, 9, 9, 9, 30];
lib/screens/elements/measurement_graph.dart
@@ -37,7 +37,9 @@ class _LineChartState extends State<_LineChart> {
data.sort((a, b) => a.creationTime.compareTo(b.creationTime));
// calculate lines for graph
- List<FlSpot> pulSpots = [], diaSpots = [], sysSpots = [];
+ final pulSpots = <FlSpot>[];
+ final diaSpots = <FlSpot>[];
+ final sysSpots = <FlSpot>[];
int maxValue = 0;
final int minValue = (settings.validateInputs ? 30 : 0);
/// Horizontally first value
@@ -256,6 +258,7 @@ class _LineChartState extends State<_LineChart> {
);
}
+// TODO: document
class MeasurementGraph extends StatelessWidget {
const MeasurementGraph({super.key, this.height = 290});
@@ -279,4 +282,3 @@ class MeasurementGraph extends StatelessWidget {
extension Sum<T> on List<T> {
double sum(num Function(T value) f) => fold<double>(0, (prev, e) => prev + f(e).toDouble());
}
-
lib/screens/error_reporting_screen.dart
@@ -6,8 +6,12 @@ import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:url_launcher/url_launcher.dart';
+/// A static location to report errors to and disrupt the program flow in case
+/// there is the risk of data loss when continuing.
class ErrorReporting {
ErrorReporting._create();
+
+ /// Whether there is already an critical error displayed.
static bool isErrorState = false;
/// Replaces the application with an ErrorScreen
@@ -35,9 +39,12 @@ class ErrorReporting {
}
}
+/// A full [MaterialApp] that is especially safe against throwing errors and
+/// allows for debugging and data extraction.
class ErrorScreen extends StatelessWidget {
const ErrorScreen({super.key, required this.title, required this.text, required this.debugInfo});
+
final String title;
final String text;
final PackageInfo debugInfo;
@@ -130,4 +137,4 @@ class ErrorScreen extends StatelessWidget {
),
);
-}
\ No newline at end of file
+}
lib/screens/home_screen.dart
@@ -18,7 +18,10 @@ import 'package:provider/provider.dart';
/// Is true during the first [AppHome.build] before creating the widget.
bool _appStart = true;
+/// Central screen of the app with graph and measurement list that is the center
+/// of navigation.
class AppHome extends StatelessWidget {
+ /// Create a home screen.
const AppHome({super.key});
@override
lib/screens/loading_screen.dart
@@ -4,7 +4,10 @@ import 'dart:ui';
import 'package:flutter/material.dart';
/// Loading page that is displayed on app start.
+///
+/// Contains a simplified app logo animation.
class LoadingScreen extends StatelessWidget {
+ /// Loading page that is displayed on app start.
const LoadingScreen({super.key});
static const _duration = Duration(milliseconds: 250);
@@ -103,4 +106,4 @@ class _LogoPainter extends CustomPainter {
return path;
}
-}
\ No newline at end of file
+}
lib/main.dart
@@ -101,43 +101,47 @@ Future<Widget> _loadApp() async {
], child: const AppRoot(),);
}
+/// Central [MaterialApp] widget of the app that sets the uniform style options.
class AppRoot extends StatelessWidget {
+ /// Create the base for the entire app.
const AppRoot({super.key});
@override
- Widget build(BuildContext context) => Consumer<Settings>(builder: (context, settings, child) => MaterialApp(
- title: 'Blood Pressure App',
- onGenerateTitle: (context) => AppLocalizations.of(context)!.title,
- theme: _buildTheme(ColorScheme.fromSeed(
- seedColor: settings.accentColor,
- ),),
- darkTheme: _buildTheme(ColorScheme.fromSeed(
- seedColor: settings.accentColor,
- brightness: Brightness.dark,
- background: Colors.black,
- ),),
- themeMode: settings.themeMode,
- localizationsDelegates: const [
- AppLocalizations.delegate,
- GlobalMaterialLocalizations.delegate,
- GlobalCupertinoLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate,
- ],
- supportedLocales: AppLocalizations.supportedLocales,
- locale: settings.language,
- home: const AppHome(),
- ),);
+ Widget build(BuildContext context) =>
+ Consumer<Settings>(builder: (context, settings, child) => MaterialApp(
+ title: 'Blood Pressure App',
+ onGenerateTitle: (context) => AppLocalizations.of(context)!.title,
+ theme: _buildTheme(ColorScheme.fromSeed(
+ seedColor: settings.accentColor,
+ ),),
+ darkTheme: _buildTheme(ColorScheme.fromSeed(
+ seedColor: settings.accentColor,
+ brightness: Brightness.dark,
+ background: Colors.black,
+ ),),
+ themeMode: settings.themeMode,
+ localizationsDelegates: const [
+ AppLocalizations.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ ],
+ supportedLocales: AppLocalizations.supportedLocales,
+ locale: settings.language,
+ home: const AppHome(),
+ ),
+ );
ThemeData _buildTheme(ColorScheme colorScheme) {
final inputBorder = OutlineInputBorder(
- borderSide: BorderSide(
- width: 3,
- // Through black background outlineVariant has enough contrast.
- color: (colorScheme.background == Colors.black)
- ? colorScheme.outlineVariant
- : colorScheme.outline,
- ),
- borderRadius: BorderRadius.circular(20),
+ borderSide: BorderSide(
+ width: 3,
+ // Through black background outlineVariant has enough contrast.
+ color: (colorScheme.background == Colors.black)
+ ? colorScheme.outlineVariant
+ : colorScheme.outline,
+ ),
+ borderRadius: BorderRadius.circular(20),
);
return ThemeData(
test/model/export_import/column_test.dart
@@ -67,7 +67,7 @@ void main() {
break;
case RowDataFieldType.needlePin:
expect(decoded.$2, isA<MeasurementNeedlePin>().having(
- (p0) => p0.toJson(), 'pin', r.needlePin?.toJson(),),);
+ (p0) => p0.toMap(), 'pin', r.needlePin?.toMap(),),);
break;
}
}
@@ -134,7 +134,7 @@ void main() {
break;
case RowDataFieldType.needlePin:
expect(decoded?.$2, isA<MeasurementNeedlePin>().having(
- (p0) => p0.toJson(), 'pin', r.needlePin?.toJson(),),);
+ (p0) => p0.toMap(), 'pin', r.needlePin?.toMap(),),);
break;
case null:
break;
@@ -182,4 +182,4 @@ BloodPressureRecord _filledRecord() => mockRecord(
pul: 789,
note: 'test',
pin: Colors.pink,
-);
\ No newline at end of file
+);
test/model/export_import/csv_converter_test.dart
@@ -320,4 +320,4 @@ List<BloodPressureRecord>? failParse(RecordParsingError error) {
case RecordParsingErrorUnparsableField():
fail('Parsing failed because field ${error.fieldContents} in line ${error.lineNumber} is not parsable.');
}
-}
\ No newline at end of file
+}
test/model/export_import/record_formatter_test.dart
@@ -26,7 +26,7 @@ void main() {
expect(ScriptedFormatter(r'$SYS',).encode(testRecord), testRecord.systolic.toString());
expect(ScriptedFormatter(r'$DIA',).encode(testRecord), testRecord.diastolic.toString());
expect(ScriptedFormatter(r'$PUL',).encode(testRecord), testRecord.pulse.toString());
- expect(ScriptedFormatter(r'$COLOR',).encode(testRecord), jsonEncode(testRecord.needlePin!.toJson()));
+ expect(ScriptedFormatter(r'$COLOR',).encode(testRecord), jsonEncode(testRecord.needlePin!.toMap()));
expect(ScriptedFormatter(r'$NOTE',).encode(testRecord), testRecord.notes);
expect(ScriptedFormatter(r'$TIMESTAMP',).encode(testRecord), testRecord.creationTime.millisecondsSinceEpoch.toString());
expect(ScriptedFormatter(r'$SYS$DIA$PUL',).encode(testRecord), (testRecord.systolic.toString()
@@ -136,4 +136,4 @@ BloodPressureRecord mockRecord({
dia,
pul,
note ?? '',
- needlePin: pin == null ? null : MeasurementNeedlePin(pin),);
\ No newline at end of file
+ needlePin: pin == null ? null : MeasurementNeedlePin(pin),);
test/model/medicine/intake_history_test.dart
@@ -169,4 +169,4 @@ void main() {
expect(deserializedHistory, history);
});
});
-}
\ No newline at end of file
+}
test/model/medicine/medicine_intake_test.dart
@@ -60,4 +60,4 @@ MedicineIntake mockIntake({
medicine: medicine ?? mockMedicine(),
dosis: dosis,
timestamp: timeMs == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(timeMs),
-);
\ No newline at end of file
+);
test/model/medicine/medicine_test.dart
@@ -64,4 +64,4 @@ Medicine mockMedicine({
final med = Medicine(_meds.length, designation: designation, color: color, defaultDosis: defaultDosis);
_meds.add(med);
return med;
-}
\ No newline at end of file
+}
test/model/analyzer_test.dart
@@ -62,4 +62,4 @@ void main() {
expect((m.lastDay), DateTime.fromMillisecondsSinceEpoch(9000000));
});
});
-}
\ No newline at end of file
+}
test/model/config_db_test.dart
@@ -80,4 +80,4 @@ void main() {
expect(newSettings.toJson(), initialSettingsJson);
});
});
-}
\ No newline at end of file
+}
test/model/convert_util_test.dart
@@ -122,4 +122,4 @@ void main() {
expect(ConvertUtil.parseThemeMode(null), null);
});
});
-}
\ No newline at end of file
+}
test/model/intervall_store_test.dart
@@ -98,4 +98,4 @@ void main() {
});
});
-}
\ No newline at end of file
+}
test/model/json_serialization_test.dart
@@ -347,4 +347,4 @@ void main() {
expect(v2.userColumns.length, ExportColumnsManager().userColumns.length);
});
});
-}
\ No newline at end of file
+}
test/ui/components/settings/dropdown_list_tile_test.dart
@@ -74,4 +74,4 @@ void main() {
expect(callCount, 1);
});
});
-}
\ No newline at end of file
+}
test/ui/components/settings/input_list_tile_test.dart
@@ -77,4 +77,4 @@ void main() {
expect(callCount, 1);
});
});
-}
\ No newline at end of file
+}
test/ui/components/settings/number_input_list_tile_test.dart
@@ -110,4 +110,4 @@ void main() {
expect(callCount, 4);
});
});
-}
\ No newline at end of file
+}
test/ui/components/settings/slider_list_tile_test.dart
@@ -55,4 +55,4 @@ void main() {
expect(callCount, 1);
});
});
-}
\ No newline at end of file
+}
test/ui/components/settings/titled_column_test.dart
@@ -44,4 +44,4 @@ void main() {
);
});
});
-}
\ No newline at end of file
+}
test/ui/components/add_export_column_dialoge_test.dart
@@ -138,4 +138,4 @@ void main() {
});
});
-}
\ No newline at end of file
+}
test/ui/components/add_measurement_dialoge_test.dart
@@ -480,4 +480,4 @@ void main() {
expect(find.descendant(of: fourthFocusedTextFormField, matching: find.text('Systolic')), findsWidgets);
});
});
-}
\ No newline at end of file
+}
test/ui/components/color_picker_test.dart
@@ -36,4 +36,4 @@ void main() {
expect(onColorSelectedCallCount, 1);
});
});
-}
\ No newline at end of file
+}
test/ui/components/enter_timeformat_dialoge_test.dart
@@ -91,4 +91,4 @@ void main() {
expect(result, 'test text!');
});
});
-}
\ No newline at end of file
+}
test/ui/components/input_dialoge_test.dart
@@ -161,4 +161,4 @@ void main() {
expect(result, null);
});
});
-}
\ No newline at end of file
+}
test/ui/components/measurement_list_entry_test.dart
@@ -62,4 +62,4 @@ void main() {
expect(find.text('null'), findsNothing);
});
});
-}
\ No newline at end of file
+}
test/ui/components/util.dart
@@ -24,4 +24,4 @@ Future<void> loadDialoge(WidgetTester tester, void Function(BuildContext context
TextButton(onPressed: () => dialogeStarter(context), child: Text(dialogeStarterText)),),),);
await tester.tap(find.text(dialogeStarterText));
await tester.pumpAndSettle();
-}
\ No newline at end of file
+}
test/ui/statistics_test.dart
@@ -88,4 +88,4 @@ Future<void> _initStatsPage(WidgetTester widgetTester, List<BloodPressureRecord>
),
),);
await widgetTester.pumpAndSettle();
-}
\ No newline at end of file
+}
test/ram_only_implementations.dart
@@ -47,4 +47,4 @@ class RamBloodPressureModel extends ChangeNotifier implements BloodPressureModel
Future<void> addAndExport(BuildContext context, BloodPressureRecord record) async {
add(record);
}
-}
\ No newline at end of file
+}
analysis_options.yaml
@@ -22,7 +22,7 @@ linter:
- eol_at_end_of_file
- leading_newlines_in_multiline_strings
- library_annotations
- - lines_longer_than_80_chars
+ # TODO: - lines_longer_than_80_chars
- matching_super_parameters
- no_literal_bool_comparisons
- noop_primitive_operations
pubspec.yaml
@@ -8,24 +8,23 @@ environment:
sdk: '>=3.0.2 <4.0.0'
dependencies:
+ csv: ^5.0.2
+ collection: ^1.17.1
+ intl: ^0.18.0
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
- provider: ^6.0.0 # MIT
- sqflite: # BSD-2-Clause
- path: # BSD-3-Clause
-
- intl: ^0.18.0 # BSD-3-Clause
- fl_chart: ^0.63.0 # MIT
- csv: ^5.0.2 # MIT
- url_launcher: ^6.1.11 # BSD-3-Clause
- shared_preferences: ^2.1.1 # BSD-3-Clause
+ flutter_markdown: ^0.6.17
+ fl_chart: ^0.63.0
+ function_tree: ^0.9.0
+ provider: ^6.0.0
+ path:
pdf: ^3.10.4
package_info_plus: ^4.0.2
- function_tree: ^0.9.0
- flutter_markdown: ^0.6.17
- collection: ^1.17.1
+ sqflite:
+ shared_preferences: ^2.1.1
+ url_launcher: ^6.1.11
# can become one custom dependency
file_picker: ^5.2.11 # MIT
@@ -34,9 +33,9 @@ dependencies:
fluttertoast: ^8.2.4
dev_dependencies:
+ file: any
flutter_test:
sdk: flutter
- file: any
flutter_lints: ^2.0.0
mockito: ^5.4.1
sqflite_common_ffi: ^2.3.0