Commit 44c3a32

derdilla <derdilla06@gmail.com>
2023-05-14 08:40:07
FEAT: basic stats screen
1 parent 3472879
lib/model/blood_pressure.dart
@@ -113,6 +113,40 @@ class BloodPressureModel extends ChangeNotifier {
     }
     return UnmodifiableListView(recordsInRange);
   }
+  
+  Future<int> get count async {
+    return (await _database.rawQuery('SELECT COUNT(*) FROM bloodPressureModel'))[0]['COUNT(*)'] as int? ?? -1;
+  }
+  Future<int> get avgDia async {
+    var res = (await _database.rawQuery('SELECT AVG(diastolic) as dia FROM bloodPressureModel'))[0]['dia'];
+    int? val;
+    try {
+      val = (res as int?);
+    } catch (e) {
+      val = (res as double?)?.toInt();
+    }
+    return val ?? -1;
+  }
+  Future<int> get avgSys async {
+    var res = (await _database.rawQuery('SELECT AVG(systolic) as sys FROM bloodPressureModel'))[0]['sys'];
+    int? val;
+    try {
+      val = (res as int?);
+    } catch (e) {
+      val = (res as double?)?.toInt();
+    }
+    return val ?? -1;
+  }
+  Future<int> get avgPul async {
+    var res = (await _database.rawQuery('SELECT AVG(pulse) as pul FROM bloodPressureModel'))[0]['pul'];
+    int? val;
+    try {
+      val = (res as int?);
+    } catch (e) {
+      val = (res as double?)?.toInt();
+    }
+    return val ?? -1;
+  }
 
   Future<void> save(void Function(bool success, String? msg) callback, {bool exportAsText = false}) async {
     // create csv
lib/screens/home.dart
@@ -1,6 +1,7 @@
 import 'package:blood_pressure_app/model/settings.dart';
 import 'package:blood_pressure_app/screens/add_measurement.dart';
 import 'package:blood_pressure_app/screens/settings.dart';
+import 'package:blood_pressure_app/screens/statistics.dart';
 import 'package:flutter/material.dart';
 import 'package:blood_pressure_app/components/measurement_graph.dart';
 import 'package:blood_pressure_app/components/measurement_list.dart';
@@ -51,51 +52,69 @@ class AppHome extends StatelessWidget {
           SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
           return Consumer<Settings>(
             builder: (context, settings, child) {
-              return SizedBox(
-                child: Column(
-                  verticalDirection: VerticalDirection.up,
-                  children: [
-                    Ink(
-                      decoration: ShapeDecoration(
-                          shape: const CircleBorder(),
-                          color: Theme.of(context).primaryColor
-                      ),
-                      child: IconButton(
-                        iconSize: settings.iconSize,
-                        icon: const Icon(
-                          Icons.add,
-                          color: Colors.black,
-                        ),
-                        onPressed: () {
-                          Navigator.push(
-                            context,
-                            MaterialPageRoute(builder: (context) => const AddMeasurementPage()),
-                          );
-                        },
+              return Column(
+                verticalDirection: VerticalDirection.up,
+                children: [
+                  Ink(
+                    decoration: ShapeDecoration(
+                      shape: const CircleBorder(),
+                      color: Theme.of(context).primaryColor
+                    ),
+                    child: IconButton(
+                      iconSize: settings.iconSize,
+                      icon: const Icon(
+                        Icons.add,
+                        color: Colors.black,
                       ),
+                      onPressed: () {
+                        Navigator.push(
+                          context,
+                          MaterialPageRoute(builder: (context) => const AddMeasurementPage()),
+                        );
+                      },
                     ),
-                    const SizedBox(height: 10,),
-                    Ink(
-                      decoration: ShapeDecoration(
-                          shape: const CircleBorder(),
-                          color: Theme.of(context).unselectedWidgetColor
+                  ),
+                  const SizedBox(height: 10,),
+                  Ink(
+                    decoration: ShapeDecoration(
+                        shape: const CircleBorder(),
+                        color: Theme.of(context).unselectedWidgetColor
+                    ),
+                    child: IconButton(
+                      iconSize: settings.iconSize,
+                      icon: const Icon(
+                          Icons.insights,
+                          color: Colors.black
                       ),
-                      child: IconButton(
-                        iconSize: settings.iconSize,
-                        icon: const Icon(
-                            Icons.settings,
-                            color: Colors.black
-                        ),
-                        onPressed: () {
-                          Navigator.push(
-                            context,
-                            MaterialPageRoute(builder: (context) => const SettingsPage()),
-                          );
-                        },
+                      onPressed: () {
+                        Navigator.push(
+                          context,
+                          MaterialPageRoute(builder: (context) => const StatisticsPage()),
+                        );
+                      },
+                    ),
+                  ),
+                  const SizedBox(height: 10,),
+                  Ink(
+                    decoration: ShapeDecoration(
+                        shape: const CircleBorder(),
+                        color: Theme.of(context).unselectedWidgetColor
+                    ),
+                    child: IconButton(
+                      iconSize: settings.iconSize,
+                      icon: const Icon(
+                          Icons.settings,
+                          color: Colors.black
                       ),
+                      onPressed: () {
+                        Navigator.push(
+                          context,
+                          MaterialPageRoute(builder: (context) => const SettingsPage()),
+                        );
+                      },
                     ),
-                  ],
-                ),
+                  ),
+                ],
               );
             }
           );
lib/screens/settings.dart
@@ -16,127 +16,125 @@ class SettingsPage extends StatelessWidget {
         backgroundColor: Theme.of(context).primaryColor,
       ),
       body: Consumer<Settings>(
-          builder: (context, settings, child) {
-            return Scaffold(
-              body: ListView(
+        builder: (context, settings, child) {
+          return ListView(
+            children: [
+              SettingsSection(
+                title: const Text('layout'),
                 children: [
-                  SettingsSection(
-                      title: const Text('layout'),
-                      children: [
-                        SwitchSettingsTile(
-                            initialValue: settings.allowManualTimeInput,
-                            onToggle: (value) {
-                              settings.allowManualTimeInput = value;
-                            },
-                            leading: const Icon(Icons.details),
-                            title: const Text('allow manual time input')
-                        ),
-                        SettingsTile(
-                          title: const Text('time format'),
-                          leading: const Icon(Icons.schedule),
-                          trailing: Icon(Icons.arrow_forward_ios, color: Theme.of(context).highlightColor,),
-                          description: Text(settings.dateFormatString),
-                          onPressed: (context) {
-                            Navigator.push(
-                              context,
-                              MaterialPageRoute(builder: (context) => const EnterTimeFormatScreen()),
-                            );
-                          },
-                        ),
-                        SwitchSettingsTile(
-                            initialValue: settings.followSystemDarkMode,
-                            onToggle: (value) {
-                              settings.followSystemDarkMode = value;
-                            },
-                            leading: const Icon(Icons.auto_mode),
-                            title: const Text('follow system dark mode')
-                        ),
-                        SwitchSettingsTile(
-                          initialValue: (() {
-                            if (settings.followSystemDarkMode) {
-                              return MediaQuery.of(context).platformBrightness == Brightness.dark;
-                            }
-                            return settings.darkMode;
-                          })(),
-                          onToggle: (value) {
-                            settings.darkMode = value;
-                          },
-                          leading: const Icon(Icons.dark_mode),
-                          title: const Text('enable dark mode'),
-                          disabled: settings.followSystemDarkMode,
-                        ),
-                        SliderSettingsTile(title: const Text('icon size'),
-                            onChanged: (double value) {
-                              settings.iconSize = value;
-                            },
-                            initialValue: settings.iconSize,
-                            start: 15,
-                            end: 70,
-                            stepSize: 5,
-                        ),
-                        ColorSelectionSettingsTile(
-                            onMainColorChanged: (color) => settings.accentColor = settings.createMaterialColor((color ?? Colors.teal).value),
-                            initialColor: settings.accentColor,
-                            title: const Text('theme color')
-                        ),
-                        ColorSelectionSettingsTile(
-                            onMainColorChanged: (color) => settings.sysColor = settings.createMaterialColor((color ?? Colors.green).value),
-                            initialColor: settings.sysColor,
-                            title: const Text('systolic color')
-                        ),
-                        ColorSelectionSettingsTile(
-                            onMainColorChanged: (color) => settings.diaColor = settings.createMaterialColor((color ?? Colors.teal).value),
-                            initialColor: settings.diaColor,
-                            title: const Text('diastolic color')
-                        ),
-                        ColorSelectionSettingsTile(
-                            onMainColorChanged: (color) => settings.pulColor = settings.createMaterialColor((color ?? Colors.red).value),
-                            initialColor: settings.pulColor,
-                            title: const Text('pulse color')
-                        ),
-                      ]
+                  SwitchSettingsTile(
+                    initialValue: settings.allowManualTimeInput,
+                    onToggle: (value) {
+                      settings.allowManualTimeInput = value;
+                    },
+                    leading: const Icon(Icons.details),
+                    title: const Text('allow manual time input')
                   ),
-                  SettingsSection(
-                    title: const Text('data'),
-                    children: [
-                      SwitchSettingsTile(
-                          initialValue: settings.useExportCompatability,
-                          title: const Text('compatability export'),
-                          description: const Text('sets export mime type to text instead of csv'),
-                          onToggle: (value) {
-                            settings.useExportCompatability = value;
-                          }
-                      ),
-                      SettingsTile(
-                        title: const Text('export'),
-                        leading: const Icon(Icons.save),
-                        onPressed: (context) =>  Provider.of<BloodPressureModel>(context, listen: false).save((success, msg) {
-                          if (success && msg != null) {
-                            ScaffoldMessenger.of(context).showSnackBar(
-                                SnackBar(content: Text(msg)));
-                          } else if (!success && msg != null) {
-                            ScaffoldMessenger.of(context).showSnackBar(
-                                SnackBar(content: Text('Error: $msg')));
-                          }
-                        }
-                            , exportAsText: settings.useExportCompatability),
-                      ),
-                      SettingsTile(
-                        title: const Text('import'),
-                        leading: const Icon(Icons.file_upload),
-                        onPressed: (context) =>  Provider.of<BloodPressureModel>(context, listen: false).import((res, String? err) {
-                          if (res) {
+                  SettingsTile(
+                    title: const Text('time format'),
+                    leading: const Icon(Icons.schedule),
+                    trailing: Icon(Icons.arrow_forward_ios, color: Theme.of(context).highlightColor,),
+                    description: Text(settings.dateFormatString),
+                    onPressed: (context) {
+                      Navigator.push(
+                        context,
+                        MaterialPageRoute(builder: (context) => const EnterTimeFormatScreen()),
+                      );
+                    },
+                  ),
+                  SwitchSettingsTile(
+                      initialValue: settings.followSystemDarkMode,
+                      onToggle: (value) {
+                        settings.followSystemDarkMode = value;
+                      },
+                      leading: const Icon(Icons.auto_mode),
+                      title: const Text('follow system dark mode')
+                  ),
+                  SwitchSettingsTile(
+                    initialValue: (() {
+                      if (settings.followSystemDarkMode) {
+                        return MediaQuery.of(context).platformBrightness == Brightness.dark;
+                      }
+                      return settings.darkMode;
+                    })(),
+                    onToggle: (value) {
+                      settings.darkMode = value;
+                    },
+                    leading: const Icon(Icons.dark_mode),
+                    title: const Text('enable dark mode'),
+                    disabled: settings.followSystemDarkMode,
+                  ),
+                  SliderSettingsTile(title: const Text('icon size'),
+                    onChanged: (double value) {
+                      settings.iconSize = value;
+                    },
+                    initialValue: settings.iconSize,
+                    start: 15,
+                    end: 70,
+                    stepSize: 5,
+                  ),
+                  ColorSelectionSettingsTile(
+                      onMainColorChanged: (color) => settings.accentColor = settings.createMaterialColor((color ?? Colors.teal).value),
+                      initialColor: settings.accentColor,
+                      title: const Text('theme color')
+                  ),
+                  ColorSelectionSettingsTile(
+                      onMainColorChanged: (color) => settings.sysColor = settings.createMaterialColor((color ?? Colors.green).value),
+                      initialColor: settings.sysColor,
+                      title: const Text('systolic color')
+                  ),
+                  ColorSelectionSettingsTile(
+                      onMainColorChanged: (color) => settings.diaColor = settings.createMaterialColor((color ?? Colors.teal).value),
+                      initialColor: settings.diaColor,
+                      title: const Text('diastolic color')
+                  ),
+                  ColorSelectionSettingsTile(
+                      onMainColorChanged: (color) => settings.pulColor = settings.createMaterialColor((color ?? Colors.red).value),
+                      initialColor: settings.pulColor,
+                      title: const Text('pulse color')
+                  ),
+                ]
+              ),
+              SettingsSection(
+                title: const Text('data'),
+                children: [
+                  SwitchSettingsTile(
+                      initialValue: settings.useExportCompatability,
+                      title: const Text('compatability export'),
+                      description: const Text('sets export mime type to text instead of csv'),
+                      onToggle: (value) {
+                        settings.useExportCompatability = value;
+                      }
+                  ),
+                  SettingsTile(
+                    title: const Text('export'),
+                    leading: const Icon(Icons.save),
+                    onPressed: (context) =>  Provider.of<BloodPressureModel>(context, listen: false).save((success, msg) {
+                      if (success && msg != null) {
+                        ScaffoldMessenger.of(context).showSnackBar(
+                            SnackBar(content: Text(msg)));
+                      } else if (!success && msg != null) {
+                        ScaffoldMessenger.of(context).showSnackBar(
+                            SnackBar(content: Text('Error: $msg')));
+                      }
+                    }
+                        , exportAsText: settings.useExportCompatability),
+                  ),
+                  SettingsTile(
+                    title: const Text('import'),
+                    leading: const Icon(Icons.file_upload),
+                    onPressed: (context) =>  Provider.of<BloodPressureModel>(context, listen: false).import((res, String? err) {
+                      if (res) {
 
-                          } else {
-                            ScaffoldMessenger.of(context).showSnackBar(
-                                SnackBar(content: Text('Error: ${err ?? 'unknown error'}')));
-                          }
-                        }),
-                      ),
-                    ],
-                  )
+                      } else {
+                        ScaffoldMessenger.of(context).showSnackBar(
+                            SnackBar(content: Text('Error: ${err ?? 'unknown error'}')));
+                      }
+                    }),
+                  ),
                 ],
-              ),
+              )
+            ],
             );
           }
       ),
lib/screens/statistics.dart
@@ -0,0 +1,118 @@
+import 'package:blood_pressure_app/model/blood_pressure.dart';
+import 'package:fl_chart/fl_chart.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+class StatisticsPage extends StatelessWidget {
+  const StatisticsPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Statistics'),
+        backgroundColor: Theme.of(context).primaryColor,
+      ),
+      body: SingleChildScrollView(
+        child: Consumer<BloodPressureModel>(
+          builder: (context, model, child) {
+            return Wrap(
+              children: [
+                Statistic(
+                    caption: const Text('Measurement count'),
+                    child: futureInt(model.count)
+                ),
+                Statistic(
+                    caption: const Text('Diastolic avg.'),
+                    child: futureInt(model.avgDia)
+                ),
+                Statistic(
+                    caption: const Text('Systolic avg.'),
+                    child: futureInt(model.avgSys)
+                ),
+                Statistic(
+                    caption: const Text('Pulse avg.'),
+                    child: futureInt(model.avgPul)
+                ),
+              ],
+            );
+          },
+        )
+      ),
+    );
+  }
+  
+  Widget futureInt(Future<int> value) {
+    return FutureBuilder<int>(
+        future: value,
+        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
+          switch (snapshot.connectionState) {
+            case ConnectionState.none:
+              return const Text('not started');
+            case ConnectionState.waiting:
+              return const Text('loading...');
+            default:
+              if (snapshot.hasError) {
+                return Text('ERROR: ${snapshot.error}');
+              }
+              assert(snapshot.hasData);
+              if ((snapshot.data??-1) < 0) {
+                return const Text('invalid data');
+              }
+              return Text(snapshot.data?.toString() ?? 'error');
+          }
+        }
+    );
+  }
+}
+
+class Statistic extends StatelessWidget {
+  final Widget caption;
+  final Widget child;
+
+  const Statistic({super.key, required this.caption, required this.child, });
+
+  @override
+  Widget build(BuildContext context) { // TODO
+    return Container(
+      margin: const EdgeInsets.only(left:20, right: 20, top: 20),
+      constraints: const BoxConstraints(
+          minHeight: 50,
+          minWidth: 150
+      ),
+      decoration: BoxDecoration(
+        border: Border.all(
+          width: 4,
+          color: Theme.of(context).textTheme.bodyMedium?.color ?? Colors.white38
+        ),
+        borderRadius: const BorderRadius.all(Radius.circular(25)),
+      ),
+      child: Stack(
+        children: [
+          Positioned(
+            top: 6,
+            left: 12,
+            child: DefaultTextStyle(
+              style: TextStyle(color: Theme.of(context).textTheme.bodyMedium?.color ?? Colors.white38),
+              child: caption
+            ),
+          ),
+          Container(
+            padding: const EdgeInsets.all(30),
+            child: Align(
+              alignment: Alignment.center,
+              child: DefaultTextStyle(
+                style: TextStyle(
+                  color: Theme.of(context).primaryColor,
+                  fontWeight: FontWeight.bold,
+                  fontSize: 40
+                ),
+                child: child,
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}