Commit 6fbebb1

derdilla <derdilla06@gmail.com>
2023-05-02 14:53:06
FEAT: save settings between sessions
1 parent 39c90cd
lib/components/measurement_graph.dart
@@ -6,211 +6,160 @@ import 'package:fl_chart/fl_chart.dart';
 import 'package:provider/provider.dart';
 import 'dart:math';
 import 'package:intl/intl.dart';
+import 'package:blood_pressure_app/model/settings.dart';
 
-class _LineChart extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() {
-    return _LineChartState();
-  }
-}
-
-class _LineChartState extends State<_LineChart> {
-  var _displayMode = DisplayModes.day;
-
+class _LineChart extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     const pulseColor = Colors.red;
     const diaColor = Colors.green;
     const sysColor = Colors.teal;
 
-
     return Stack(
       children: [
-        Container(
-          height: 200,
-          child: Consumer<BloodPressureModel>(
-              builder: (context, model, child) {
-                late final Future<UnmodifiableListView<BloodPressureRecord>> dataFuture;
-                DateTime now = DateTime.now();
-                switch (_displayMode) {
-                  case DisplayModes.day:
-                    dataFuture = model.getInTimeRange(DateTime(now.year, now.month, now.day), now);
-                    break;
-                  case DisplayModes.month:
-                    dataFuture = model.getInTimeRange(DateTime(now.year, now.month), now);
-                    break;
-                  case DisplayModes.year:
-                    dataFuture = model.getInTimeRange(DateTime(now.year), now);
-                    break;
-                  case DisplayModes.lifetime:
-                    dataFuture = model.getInTimeRange(DateTime.fromMillisecondsSinceEpoch(0), now);
-                    break;
-                }
-
-
-                return FutureBuilder<UnmodifiableListView<BloodPressureRecord>>(
-                    future: dataFuture,
-                    builder: (BuildContext context, AsyncSnapshot<UnmodifiableListView<BloodPressureRecord>> snapshot) {
-                      Widget res;
-                      switch (snapshot.connectionState) {
-                        case ConnectionState.none:
-                          res = const Text('not started');
+        Align(
+          alignment: Alignment.topCenter,
+          child: Container(
+              height: 200,
+              child: Consumer<Settings>(
+                builder: (context, settings, child) {
+                  return Consumer<BloodPressureModel>(
+                    builder: (context, model, child) {
+                      late final Future<UnmodifiableListView<BloodPressureRecord>> dataFuture;
+                      DateTime now = DateTime.now();
+                      switch (settings.graphStepSize) {
+                        case TimeStep.day:
+                          dataFuture = model.getInTimeRange(DateTime(now.year, now.month, now.day), now);
                           break;
-                        case ConnectionState.waiting:
-                          res = const Text('loading...');
+                        case TimeStep.month:
+                          dataFuture = model.getInTimeRange(DateTime(now.year, now.month), now);
                           break;
-                        default:
-                          if (snapshot.hasError) {
-                            res = Text('ERROR: ${snapshot.error}');
-                          } else {
-                            assert(snapshot.hasData);
-                            final data = snapshot.data ?? [];
-
-                            List<FlSpot> pulseSpots = [];
-                            List<FlSpot> diastolicSpots = [];
-                            List<FlSpot> systolicSpots = [];
-                            int pulMax = 0;
-                            int diaMax = 0;
-                            int sysMax = 0;
-                            for (var element in data) {
-                              final x = element.creationTime.millisecondsSinceEpoch.toDouble();
-                              diastolicSpots.add(FlSpot(x, element.diastolic.toDouble()));
-                              systolicSpots.add(FlSpot(x, element.systolic.toDouble()));
-                              pulseSpots.add(FlSpot(x, element.pulse.toDouble()));
-                              pulMax = max(pulMax, element.pulse);
-                              diaMax = max(diaMax, element.diastolic);
-                              sysMax = max(sysMax, element.systolic);
-                            }
-
+                        case TimeStep.year:
+                          dataFuture = model.getInTimeRange(DateTime(now.year), now);
+                          break;
+                        case TimeStep.lifetime:
+                          dataFuture = model.getInTimeRange(DateTime.fromMillisecondsSinceEpoch(0), now);
+                          break;
+                      }
 
-                            final noTitels = AxisTitles(sideTitles: SideTitles(reservedSize: 40, showTitles: false));
-                            res = LineChart(
-                                swapAnimationDuration: const Duration(milliseconds: 250),
-                                LineChartData(
-                                    minY: 30,
-                                    maxY: max(pulMax.toDouble(), max(diaMax.toDouble(), sysMax.toDouble())) + 5,
-                                    titlesData: FlTitlesData(topTitles: noTitels, rightTitles:  noTitels,
-                                        bottomTitles: AxisTitles(
-                                          sideTitles: SideTitles(
-                                              showTitles: true,
-                                              getTitlesWidget: (double pos, TitleMeta meta) {
-                                                late final DateFormat formater;
-                                                switch (_displayMode) {
-                                                  case DisplayModes.day:
-                                                    formater = DateFormat('H:mm');
-                                                    break;
-                                                  case DisplayModes.month:
-                                                    formater = DateFormat('d');
-                                                    break;
-                                                  case DisplayModes.year:
-                                                    formater = DateFormat('MMM');
-                                                    break;
-                                                  case DisplayModes.lifetime:
-                                                    formater = DateFormat('yyyy');
-                                                }
-                                                return Text(
-                                                    formater.format(DateTime.fromMillisecondsSinceEpoch(pos.toInt()))
-                                                );
-                                              }
+                      return FutureBuilder<UnmodifiableListView<BloodPressureRecord>>(
+                          future: dataFuture,
+                          builder: (BuildContext context, AsyncSnapshot<UnmodifiableListView<BloodPressureRecord>> snapshot) {
+                            Widget res;
+                            switch (snapshot.connectionState) {
+                              case ConnectionState.none:
+                                res = const Text('not started');
+                                break;
+                              case ConnectionState.waiting:
+                                res = const Text('loading...');
+                                break;
+                              default:
+                                if (snapshot.hasError) {
+                                  res = Text('ERROR: ${snapshot.error}');
+                                } else {
+                                  assert(snapshot.hasData);
+                                  final data = snapshot.data ?? [];
+
+                                  List<FlSpot> pulseSpots = [];
+                                  List<FlSpot> diastolicSpots = [];
+                                  List<FlSpot> systolicSpots = [];
+                                  int pulMax = 0;
+                                  int diaMax = 0;
+                                  int sysMax = 0;
+                                  for (var element in data) {
+                                    final x = element.creationTime.millisecondsSinceEpoch.toDouble();
+                                    diastolicSpots.add(FlSpot(x, element.diastolic.toDouble()));
+                                    systolicSpots.add(FlSpot(x, element.systolic.toDouble()));
+                                    pulseSpots.add(FlSpot(x, element.pulse.toDouble()));
+                                    pulMax = max(pulMax, element.pulse);
+                                    diaMax = max(diaMax, element.diastolic);
+                                    sysMax = max(sysMax, element.systolic);
+                                  }
+
+
+                                  final noTitels = AxisTitles(sideTitles: SideTitles(reservedSize: 40, showTitles: false));
+                                  res = LineChart(
+                                      swapAnimationDuration: const Duration(milliseconds: 250),
+                                      LineChartData(
+                                          minY: 30,
+                                          maxY: max(pulMax.toDouble(), max(diaMax.toDouble(), sysMax.toDouble())) + 5,
+                                          titlesData: FlTitlesData(topTitles: noTitels, rightTitles:  noTitels,
+                                              bottomTitles: AxisTitles(
+                                                sideTitles: SideTitles(
+                                                    showTitles: true,
+                                                    getTitlesWidget: (double pos, TitleMeta meta) {
+                                                      late final DateFormat formater;
+                                                      switch (settings.graphStepSize) {
+                                                        case TimeStep.day:
+                                                          formater = DateFormat('H:mm');
+                                                          break;
+                                                        case TimeStep.month:
+                                                          formater = DateFormat('d');
+                                                          break;
+                                                        case TimeStep.year:
+                                                          formater = DateFormat('MMM');
+                                                          break;
+                                                        case TimeStep.lifetime:
+                                                          formater = DateFormat('yyyy');
+                                                      }
+                                                      return Text(
+                                                          formater.format(DateTime.fromMillisecondsSinceEpoch(pos.toInt()))
+                                                      );
+                                                    }
+                                                ),
+                                              )
                                           ),
-                                        )
-                                    ),
-                                    lineBarsData: [
-                                      // high blood pressure marking acordning to https://www.texasheart.org/heart-health/heart-information-center/topics/high-blood-pressure-hypertension/
-                                      LineChartBarData(
-                                          spots: pulseSpots,
-                                          color: pulseColor,
-                                          barWidth: 4,
-                                          isCurved: true,
-                                          preventCurveOverShooting: true
-                                      ),
-                                      LineChartBarData(
-                                          spots: diastolicSpots,
-                                          color: diaColor,
-                                          barWidth: 4,
-                                          isCurved: true,
-                                          preventCurveOverShooting: true,
-                                          belowBarData: BarAreaData(
-                                              show: true,
-                                              color: Colors.red.shade400.withAlpha(100),
-                                              cutOffY: 80,
-                                              applyCutOffY: true
-                                          )
-                                      ),
-                                      LineChartBarData(
-                                          spots: systolicSpots,
-                                          color: sysColor,
-                                          barWidth: 4,
-                                          isCurved: true,
-                                          preventCurveOverShooting: true,
-                                          belowBarData: BarAreaData(
-                                              show: true,
-                                              color: Colors.red.shade400.withAlpha(100),
-                                              cutOffY: 130,
-                                              applyCutOffY: true
-                                          )
+                                          lineBarsData: [
+                                            // high blood pressure marking acordning to https://www.texasheart.org/heart-health/heart-information-center/topics/high-blood-pressure-hypertension/
+                                            LineChartBarData(
+                                                spots: pulseSpots,
+                                                color: pulseColor,
+                                                barWidth: 4,
+                                                isCurved: true,
+                                                preventCurveOverShooting: true
+                                            ),
+                                            LineChartBarData(
+                                                spots: diastolicSpots,
+                                                color: diaColor,
+                                                barWidth: 4,
+                                                isCurved: true,
+                                                preventCurveOverShooting: true,
+                                                belowBarData: BarAreaData(
+                                                    show: true,
+                                                    color: Colors.red.shade400.withAlpha(100),
+                                                    cutOffY: 80,
+                                                    applyCutOffY: true
+                                                )
+                                            ),
+                                            LineChartBarData(
+                                                spots: systolicSpots,
+                                                color: sysColor,
+                                                barWidth: 4,
+                                                isCurved: true,
+                                                preventCurveOverShooting: true,
+                                                belowBarData: BarAreaData(
+                                                    show: true,
+                                                    color: Colors.red.shade400.withAlpha(100),
+                                                    cutOffY: 130,
+                                                    applyCutOffY: true
+                                                )
+                                            )
+                                          ]
                                       )
-                                    ]
-                                )
-                            );
+                                  );
+                                }
+                            }
+                            return res;
                           }
-                      }
-                      return res;
+                      );
                     }
-                );
-              },
-        )
+                  );
+                },
+              )
+          ),
         ),
-        Align(
-          alignment: Alignment.topRight,
-          child: Container(
-            color: Colors.white,
-            child: DropdownButton<int>(
-              value: _displayMode,
-              onChanged: (int? value) {
-                setState(() {
-                  _displayMode = value ?? 1;
-                });
-              },
-              dropdownColor: Colors.white,
-              items: DisplayModes.options.map<DropdownMenuItem<int>>((v) {
-                return DropdownMenuItem(
-                    value: v,
-                    child: Text(
-                        DisplayModes.getName(v)
-                    )
-                );
-              }).toList(),
-            ),
-          )
-        )
       ],
     );
-
-  }
-
-}
-
-class DisplayModes {
-  static const options = [0, 1, 2, 3];
-
-  static const day = 0;
-  static const month = 1;
-  static const year = 2;
-  static const lifetime = 3;
-
-  static String getName(int opt) {
-    switch (opt) {
-      case day:
-        return 'day';
-      case month:
-        return 'month';
-      case year:
-        return 'year';
-      case lifetime:
-        return 'lifetime';
-    }
-    return 'invalid';
   }
 }
 
@@ -221,10 +170,38 @@ class MeasurementGraph extends StatelessWidget {
   Widget build(BuildContext context) {
     // TODO: implement build
     return SizedBox(
-      height: 100,
+      height: 1000,
       child: Padding(
-        padding: const EdgeInsets.only(right: 16, left: 6, top: 10),
-        child: _LineChart(),
+        padding: const EdgeInsets.only(right: 16, left: 6, top: 2),
+        child: Column(
+          children: [
+            _LineChart(),
+            Consumer<Settings>(
+                builder: (context, settings, child) {
+                  return Container(
+                    color: Colors.white,
+                    child: DropdownButton<int>(
+                      value: settings.graphStepSize,
+                      onChanged: (int? value) {
+                        if (value != null) {
+                          settings.graphStepSize = value;
+                        }
+                      },
+                      dropdownColor: Colors.white,
+                      items: TimeStep.options.map<DropdownMenuItem<int>>((v) {
+                        return DropdownMenuItem(
+                            value: v,
+                            child: Text(
+                                TimeStep.getName(v)
+                            )
+                        );
+                      }).toList(),
+                    ),
+                  );
+                }
+            )
+          ],
+        ),
       ),
     );
   }
lib/model/blood_pressure.dart
@@ -63,7 +63,7 @@ class BloodPressureModel extends ChangeNotifier {
       measurement.pulse,
       measurement.notes]);
 
-    _cacheLast();
+    _cacheLast(); // hast notifyListeners()
   }
 
   /// Returns the last x BloodPressureRecords from new to old.
lib/model/settings.dart
@@ -0,0 +1,94 @@
+
+import 'dart:collection';
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:path/path.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
+import 'package:sqflite/sqflite.dart';
+import 'package:file_saver/file_saver.dart';
+import 'package:share_plus/share_plus.dart';
+import 'dart:convert' show utf8;
+import 'dart:typed_data';
+
+class Settings extends ChangeNotifier {
+  static const maxEntries = 2E64; // https://www.sqlite.org/limits.html Nr.13
+  late final Database _database;
+
+  late int _graphStepSize;
+
+  Settings._create();
+  Future<void> _asyncInit() async {
+    _database = await openDatabase(
+      join(await getDatabasesPath(), 'settings.db'),
+      // runs when the database is first created
+      onCreate: (db, version) {
+        return db.execute('CREATE TABLE settings(key STRING PRIMARY KEY, value STRING)');
+      },
+      version: 1,
+    );
+    await _loadSettings();
+  }
+  // factory method, to allow for async contructor
+  static Future<Settings> create() async {
+    final component = Settings._create();
+    await component._asyncInit();
+    return component;
+  }
+
+  Future<Object?> _getSetting(String key) async {
+    final r = await _database.query('settings', where: 'key = ?', whereArgs: [key]);
+    if (r.isNotEmpty) {
+      return r[0]['value'];
+    }
+  }
+
+  Future<void> _saveSetting(String key, Object value) async {
+    if ((await _database.query('settings', where: 'key = ?', whereArgs: [key])).isEmpty) {
+      _database.insert('settings', {'key': key, 'value': value});
+    } else {
+      _database.update('settings', {'value': value}, where: 'key = ?', whereArgs: [key]);
+    }
+  }
+
+  Future<void> _loadSettings() async {
+    var pGraphStepSize = _getSetting('_graphStepSize');
+    // var ...
+
+    _graphStepSize = (await pGraphStepSize as int?) ?? TimeStep.day;
+    // ...
+  }
+
+  int get graphStepSize {
+    return _graphStepSize;
+  }
+  set graphStepSize(int newStepSize) {
+    _graphStepSize = newStepSize;
+    _saveSetting('_graphStepSize', newStepSize);
+    notifyListeners();
+  }
+
+}
+
+class TimeStep {
+  static const options = [0, 1, 2, 3];
+
+  static const day = 0;
+  static const month = 1;
+  static const year = 2;
+  static const lifetime = 3;
+
+  static String getName(int opt) {
+    switch (opt) {
+      case day:
+        return 'day';
+      case month:
+        return 'month';
+      case year:
+        return 'year';
+      case lifetime:
+        return 'lifetime';
+    }
+    return 'invalid';
+  }
+}
\ No newline at end of file
lib/main.dart
@@ -1,3 +1,4 @@
+import 'package:blood_pressure_app/model/settings.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:blood_pressure_app/model/blood_pressure.dart';
@@ -16,13 +17,18 @@ void main() async {
     databaseFactory = databaseFactoryFfi;
   }
 
+  // 2 different db files
   final dataModel = await BloodPressureModel.create();
+  final settingsModel = await Settings.create();
 
   runApp(
-      ChangeNotifierProvider(
-        create: (context) => dataModel,
-        child: const AppRoot(),
-      ),
+      MultiProvider(
+        providers: [
+          ChangeNotifierProvider(create: (context) => dataModel),
+          ChangeNotifierProvider(create: (context) => settingsModel),
+        ],
+        child: const AppRoot()
+      )
   );
 }