main
  1import 'dart:collection';
  2import 'dart:convert';
  3
  4import 'package:blood_pressure_app/config.dart';
  5import 'package:blood_pressure_app/features/bluetooth/logic/device_scan_cubit.dart';
  6import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
  7import 'package:blood_pressure_app/model/horizontal_graph_line.dart';
  8import 'package:blood_pressure_app/model/storage/bluetooth_input_mode.dart';
  9import 'package:blood_pressure_app/model/storage/convert_util.dart';
 10import 'package:blood_pressure_app/model/weight_unit.dart';
 11import 'package:flutter/material.dart';
 12
 13/// Stores settings that are directly controllable by the user through the
 14/// settings screen.
 15///
 16/// This class should not be used to save persistent state of individual app
 17/// components and screens.
 18///
 19/// The `storage.dart` library comment has more information on the architecture
 20/// for adding persistent fields.
 21class Settings extends ChangeNotifier {
 22  /// Creates a settings object with the default values.
 23  ///
 24  /// When the values should be set consider using the factory methods.
 25  Settings({
 26    Locale? language,
 27    Color? accentColor,
 28    Color? sysColor,
 29    Color? diaColor,
 30    Color? pulColor,
 31    List<HorizontalGraphLine>? horizontalGraphLines,
 32    String? dateFormatString,
 33    double? graphLineThickness,
 34    double? needlePinBarWidth,
 35    int? animationSpeed,
 36    int? sysWarn,
 37    int? diaWarn,
 38    int? lastVersion,
 39    bool? allowManualTimeInput,
 40    bool? confirmDeletion,
 41    ThemeMode? themeMode,
 42    bool? validateInputs,
 43    bool? allowMissingValues,
 44    bool? drawRegressionLines,
 45    bool? startWithAddMeasurementPage,
 46    bool? useLegacyList,
 47    bool? bottomAppBars,
 48    PressureUnit? preferredPressureUnit,
 49    List<String>? knownBleDev,
 50    int? highestMedIndex,
 51    BluetoothInputMode? bleInput,
 52    bool? weightInput,
 53    WeightUnit? weightUnit,
 54    bool? trustBLETime,
 55    bool? showBLETimeTrustDialog,
 56  }) {
 57    if (accentColor != null) _accentColor = accentColor;
 58    if (sysColor != null) _sysColor = sysColor;
 59    if (diaColor != null) _diaColor = diaColor;
 60    if (pulColor != null) _pulColor = pulColor;
 61    if (allowManualTimeInput != null) _allowManualTimeInput = allowManualTimeInput;
 62    if (confirmDeletion != null) _confirmDeletion = confirmDeletion;
 63    if (themeMode != null) _themeMode = themeMode;
 64    if (dateFormatString != null) _dateFormatString = dateFormatString;
 65    if (animationSpeed != null) _animationSpeed = animationSpeed;
 66    if (sysWarn != null) _sysWarn = sysWarn;
 67    if (diaWarn != null) _diaWarn = diaWarn;
 68    if (graphLineThickness != null) _graphLineThickness = graphLineThickness;
 69    if (needlePinBarWidth != null) _needlePinBarWidth = needlePinBarWidth;
 70    if (validateInputs != null) _validateInputs = validateInputs;
 71    if (allowMissingValues != null) _allowMissingValues = allowMissingValues;
 72    if (drawRegressionLines != null) _drawRegressionLines = drawRegressionLines;
 73    if (startWithAddMeasurementPage != null) _startWithAddMeasurementPage = startWithAddMeasurementPage;
 74    if (useLegacyList != null) _useLegacyList = useLegacyList;
 75    if (horizontalGraphLines != null) _horizontalGraphLines = horizontalGraphLines;
 76    if (lastVersion != null) _lastVersion = lastVersion;
 77    if (bottomAppBars != null) _bottomAppBars = bottomAppBars;
 78    if (preferredPressureUnit != null) _preferredPressureUnit = preferredPressureUnit;
 79    if (highestMedIndex != null) _highestMedIndex = highestMedIndex;
 80    if (knownBleDev != null) _knownBleDev = knownBleDev;
 81    if (bleInput != null) _bleInput = bleInput;
 82    if (weightInput != null) _weightInput = weightInput;
 83    if (weightUnit != null) _weightUnit = weightUnit;
 84    if (trustBLETime != null) _trustBLETime = trustBLETime;
 85    if (showBLETimeTrustDialog != null) _showBLETimeTrustDialog = showBLETimeTrustDialog;
 86    _language = language; // No check here, as null is the default as well.
 87  }
 88
 89  /// Create a instance from a map created by [toMap].
 90  factory Settings.fromMap(Map<String, dynamic> map) {
 91    final settingsObject = Settings(
 92      accentColor: ConvertUtil.parseColor(map['accentColor']),
 93      sysColor: ConvertUtil.parseColor(map['sysColor']),
 94      diaColor: ConvertUtil.parseColor(map['diaColor']),
 95      pulColor: ConvertUtil.parseColor(map['pulColor']),
 96      allowManualTimeInput: ConvertUtil.parseBool(map['allowManualTimeInput']),
 97      confirmDeletion: ConvertUtil.parseBool(map['confirmDeletion']),
 98      themeMode: ConvertUtil.parseThemeMode(map['themeMode']),
 99      dateFormatString: ConvertUtil.parseString(map['dateFormatString']),
100      animationSpeed: ConvertUtil.parseInt(map['animationSpeed']),
101      sysWarn: ConvertUtil.parseInt(map['sysWarn']),
102      diaWarn: ConvertUtil.parseInt(map['diaWarn']),
103      graphLineThickness: ConvertUtil.parseDouble(map['graphLineThickness']),
104      validateInputs: ConvertUtil.parseBool(map['validateInputs']),
105      allowMissingValues: ConvertUtil.parseBool(map['allowMissingValues']),
106      drawRegressionLines: ConvertUtil.parseBool(map['drawRegressionLines']),
107      startWithAddMeasurementPage: ConvertUtil.parseBool(map['startWithAddMeasurementPage']),
108      useLegacyList: ConvertUtil.parseBool(map['useLegacyList']),
109      language: ConvertUtil.parseLocale(map['language']),
110      horizontalGraphLines: ConvertUtil.parseList<String>(map['horizontalGraphLines'])?.map((e) =>
111          HorizontalGraphLine.fromJson(jsonDecode(e)),).toList(),
112      needlePinBarWidth: ConvertUtil.parseDouble(map['needlePinBarWidth']),
113      lastVersion: ConvertUtil.parseInt(map['lastVersion']),
114      bottomAppBars: ConvertUtil.parseBool(map['bottomAppBars']),
115      highestMedIndex: ConvertUtil.parseInt(map['highestMedIndex']),
116      knownBleDev: ConvertUtil.parseList<String>(map['knownBleDev']),
117      bleInput: BluetoothInputMode.deserialize(ConvertUtil.parseInt(map['bleInput'])),
118      weightInput: ConvertUtil.parseBool(map['weightInput']),
119      preferredPressureUnit: PressureUnit.decode(ConvertUtil.parseInt(map['preferredPressureUnit'])),
120      weightUnit: WeightUnit.deserialize(ConvertUtil.parseInt(map['weightUnit'])),
121      trustBLETime: ConvertUtil.parseBool(map['trustBLETime']),
122      showBLETimeTrustDialog: ConvertUtil.parseBool(map['showBLETimeTrustDialog']),
123    );
124
125    // update
126    if (ConvertUtil.parseBool(map['followSystemThemeMode']) == false) { // when this is true the default is the same
127      settingsObject.themeMode = (ConvertUtil.parseBool(map['themeMode']) ?? true) ? ThemeMode.dark : ThemeMode.light;
128    }
129    return settingsObject;
130  }
131
132  /// Create a instance from a [String] created by [toJson].
133  factory Settings.fromJson(String json) {
134    try {
135      return Settings.fromMap(jsonDecode(json));
136    } catch (exception) {
137      return Settings();
138    }
139  }
140
141  /// Serialize the object to a restoreable map.
142  Map<String, dynamic> toMap() => <String, dynamic>{
143    'accentColor': accentColor.toARGB32(),
144    'sysColor': sysColor.toARGB32(),
145    'diaColor': diaColor.toARGB32(),
146    'pulColor': pulColor.toARGB32(),
147    'dateFormatString': dateFormatString,
148    'graphLineThickness': graphLineThickness,
149    'animationSpeed': animationSpeed,
150    'sysWarn': sysWarn,
151    'diaWarn': diaWarn,
152    'allowManualTimeInput': allowManualTimeInput,
153    'confirmDeletion': confirmDeletion,
154    'themeMode': themeMode.serialize(),
155    'validateInputs': validateInputs,
156    'allowMissingValues': allowMissingValues,
157    'drawRegressionLines': drawRegressionLines,
158    'startWithAddMeasurementPage': startWithAddMeasurementPage,
159    'useLegacyList': compactList,
160    'language': ConvertUtil.serializeLocale(language),
161    'horizontalGraphLines': horizontalGraphLines.map(jsonEncode).toList(),
162    'needlePinBarWidth': _needlePinBarWidth,
163    'lastVersion': lastVersion,
164    'bottomAppBars': bottomAppBars,
165    'highestMedIndex': highestMedIndex,
166    'preferredPressureUnit': preferredPressureUnit.encode(),
167    'knownBleDev': knownBleDev,
168    'bleInput': bleInput.serialize(),
169    'weightInput': weightInput,
170    'weightUnit': weightUnit.serialized,
171    'trustBLETime': trustBLETime,
172    'showBLETimeTrustDialog': showBLETimeTrustDialog,
173  };
174
175  /// Serialize the object to a restoreable string.
176  String toJson() => jsonEncode(toMap());
177
178  /// Copy all values from another instance.
179  void copyFrom(Settings other) {
180    _language = other._language;
181    _accentColor = other._accentColor;
182    _sysColor = other._sysColor;
183    _diaColor = other._diaColor;
184    _pulColor = other._pulColor;
185    _horizontalGraphLines = other._horizontalGraphLines;
186    _dateFormatString = other._dateFormatString;
187    _graphLineThickness = other._graphLineThickness;
188    _needlePinBarWidth = other._needlePinBarWidth;
189    _animationSpeed = other._animationSpeed;
190    _sysWarn = other._sysWarn;
191    _diaWarn = other._diaWarn;
192    _lastVersion = other._lastVersion;
193    _allowManualTimeInput = other._allowManualTimeInput;
194    _confirmDeletion = other._confirmDeletion;
195    _themeMode = other._themeMode;
196    _validateInputs = other._validateInputs;
197    _allowMissingValues = other._allowMissingValues;
198    _drawRegressionLines = other._drawRegressionLines;
199    _startWithAddMeasurementPage = other._startWithAddMeasurementPage;
200    _useLegacyList = other._useLegacyList;
201    _bottomAppBars = other._bottomAppBars;
202    _preferredPressureUnit = other._preferredPressureUnit;
203    _knownBleDev = other._knownBleDev;
204    _bleInput = other._bleInput;
205    _highestMedIndex = other._highestMedIndex;
206    _weightInput = other._weightInput;
207    _weightUnit = other._weightUnit;
208    _trustBLETime = other._trustBLETime;
209    _showBLETimeTrustDialog = other._showBLETimeTrustDialog;
210    notifyListeners();
211  }
212
213  /// Reset all fields to their default values.
214  void reset() => copyFrom(Settings());
215
216  Locale? _language;
217  /// Language to use the app in.
218  ///
219  /// When the value is null, the device default language is chosen.
220  Locale? get language => _language;
221  set language(Locale? value) {
222    _language = value;
223    notifyListeners();
224  }
225
226  Color _accentColor = Colors.teal;
227  /// The primary theme color of the app.
228  Color get accentColor => _accentColor;
229  set accentColor(Color newColor) {
230    _accentColor = newColor;
231    notifyListeners();
232  }
233
234  Color _sysColor = Colors.teal;
235  /// The color of the systolic line in graphs and list headlines.
236  Color get sysColor => _sysColor;
237  set sysColor(Color newColor) {
238    _sysColor = newColor;
239    notifyListeners();
240  }
241
242  Color _diaColor = Colors.green;
243  /// The color of the diastolic line in graphs and list headlines.
244  Color get diaColor => _diaColor;
245  set diaColor(Color newColor) {
246    _diaColor = newColor;
247    notifyListeners();
248  }
249
250  Color _pulColor = Colors.red;
251  /// The color of the pulse line in graphs and list headlines.
252  Color get pulColor => _pulColor;
253  set pulColor(Color newColor) {
254    _pulColor = newColor;
255    notifyListeners();
256  }
257
258  List<HorizontalGraphLine> _horizontalGraphLines = [];
259  /// Lines that are drawn horizontally in the graph that indicate height.
260  List<HorizontalGraphLine> get horizontalGraphLines => _horizontalGraphLines;
261  // TODO: change so it is similar to medicine
262  set horizontalGraphLines(List<HorizontalGraphLine> value) {
263    _horizontalGraphLines = value;
264    notifyListeners();
265  }
266
267  String _dateFormatString = 'yyyy-MM-dd HH:mm';
268  /// The time format to use when a human readable time is required.
269  String get dateFormatString => _dateFormatString;
270  set dateFormatString(String value) {
271    _dateFormatString = value;
272    notifyListeners();
273  }
274
275  double _graphLineThickness = 3;
276  /// The width of value lines in the graph.
277  ///
278  /// Does not apply for all markers.
279  double get graphLineThickness => _graphLineThickness;
280  set graphLineThickness(double value) {
281    _graphLineThickness = value;
282    notifyListeners();
283  }
284
285  int _animationSpeed = 150;
286  /// Time in which animations run. Higher => slower.
287  ///
288  /// Usually between 0 and 1000.
289  int get animationSpeed => _animationSpeed;
290  set animationSpeed(int value) {
291    _animationSpeed = value;
292    notifyListeners();
293  }
294
295  int _sysWarn = 120;
296  /// The height from which to highlight the area below for the systolic line
297  /// in the graph.
298  int get sysWarn => _sysWarn;
299  set sysWarn(int value) {
300    _sysWarn = value;
301    notifyListeners();
302  }
303
304  int _diaWarn = 80;
305  /// The height from which to highlight the area below for the diastolic line
306  /// in the graph.
307  int get diaWarn => _diaWarn;
308  set diaWarn(int value) {
309    _diaWarn = value;
310    notifyListeners();
311  }
312
313  int _lastVersion = 0;
314  /// (META) The last version to which settings are upgraded.
315  ///
316  /// Gets to the latest version on app start, after upgrades have run.
317  int get lastVersion => _lastVersion;
318  set lastVersion(int value) {
319    _lastVersion = value;
320    notifyListeners();
321  }
322
323  bool _allowManualTimeInput = true;
324  /// Whether to show the time editor on the add entry page.
325  bool get allowManualTimeInput => _allowManualTimeInput;
326  set allowManualTimeInput(bool value) {
327    _allowManualTimeInput = value;
328    notifyListeners();
329  }
330
331  bool _confirmDeletion = true;
332  /// Whether to show a dialoge that requires confirmation before deleting
333  /// entries.
334  bool get confirmDeletion => _confirmDeletion;
335  set confirmDeletion(bool value) {
336    _confirmDeletion = value;
337    notifyListeners();
338  }
339
340  ThemeMode _themeMode = ThemeMode.system;
341  /// What color theme the whole app should use.
342  ThemeMode get themeMode => _themeMode;
343  set themeMode(ThemeMode value) {
344    _themeMode = value;
345    notifyListeners();
346  }
347
348
349  bool _validateInputs = true;
350  /// Whether to run any validators on values inputted on add measurement page.
351  bool get validateInputs => _validateInputs;
352  set validateInputs(bool value) {
353    _validateInputs = value;
354    notifyListeners();
355  }
356
357  bool _allowMissingValues = false;
358  /// Whether to allow not filling all fields on the add measurement page.
359  ///
360  /// When this is true [validateInputs] must be set to false in order for this
361  /// to take effect.
362  bool get allowMissingValues => _allowMissingValues;
363  set allowMissingValues(bool value) {
364    _allowMissingValues = value;
365    notifyListeners();
366  }
367
368  bool _drawRegressionLines = false;
369  /// Whether to draw trend lines on the graph.
370  bool get drawRegressionLines => _drawRegressionLines;
371  set drawRegressionLines(bool value) {
372    _drawRegressionLines = value;
373    notifyListeners();
374  }
375
376  bool _startWithAddMeasurementPage = false;
377  /// Whether to show the add measurement page on app launch.
378  bool get startWithAddMeasurementPage => _startWithAddMeasurementPage;
379  set startWithAddMeasurementPage(bool value) {
380    _startWithAddMeasurementPage = value;
381    notifyListeners();
382  }
383
384  bool _useLegacyList = false;
385  /// Whether to use the compact list with swipe deletion.
386  bool get compactList => _useLegacyList;
387  set compactList(bool value) {
388    _useLegacyList = value;
389    notifyListeners();
390  }
391
392  double _needlePinBarWidth = 5;
393  /// The width the color of measurements should have on the graph.
394  double get needlePinBarWidth => _needlePinBarWidth;
395  set needlePinBarWidth(double value) {
396    _needlePinBarWidth = value;
397    notifyListeners();
398  }
399
400  bool _bottomAppBars = false;
401  /// Whether to put the app bar in dialoges at the bottom of the screen.
402  bool get bottomAppBars => _bottomAppBars;
403  set bottomAppBars(bool value) {
404    _bottomAppBars = value;
405    notifyListeners();
406  }
407
408  PressureUnit _preferredPressureUnit = PressureUnit.mmHg;
409  /// Preferred unit to display and enter measurements in.
410  PressureUnit get preferredPressureUnit => _preferredPressureUnit;
411  set preferredPressureUnit(PressureUnit value) {
412    _preferredPressureUnit = value;
413    notifyListeners();
414  }
415
416  BluetoothInputMode _bleInput = BluetoothInputMode.disabled;
417  /// Whether to show bluetooth input on add measurement page.
418  BluetoothInputMode get bleInput => _bleInput;
419  set bleInput(BluetoothInputMode value) {
420    if (isPlatformSupportedBluetooth) _bleInput = value;
421    notifyListeners();
422  }
423
424  bool _weightInput = false;
425  /// Whether to show weight related features.
426  bool get weightInput => _weightInput;
427  set weightInput(bool value) {
428    _weightInput = value;
429    notifyListeners();
430  }
431
432  List<String> _knownBleDev = [];
433  /// Bluetooth devices that previously connected.
434  ///
435  /// The exact value that is stored here is determined in [DeviceScanCubit].
436  UnmodifiableListView<String> get knownBleDev =>
437      UnmodifiableListView(_knownBleDev);
438  set knownBleDev(List<String> value) {
439    _knownBleDev = value;
440    notifyListeners();
441  }
442
443  int _highestMedIndex = 0;
444  /// Total amount of medicines created.
445  int get highestMedIndex => _highestMedIndex;
446
447  WeightUnit _weightUnit = WeightUnit.kg;
448  /// Preferred unit for bodyweight.
449  WeightUnit get weightUnit => _weightUnit;
450  set weightUnit(WeightUnit value) {
451    _weightUnit = value;
452    notifyListeners();
453  }
454
455
456  bool _trustBLETime = true;
457  /// Whether to autofill the time the bluetooth device reports.
458  ///
459  /// This was introduced because the system time tends to be more accurate.
460  bool get trustBLETime => _trustBLETime;
461  set trustBLETime(bool value) {
462    _trustBLETime = value;
463    notifyListeners();
464  }
465
466  bool _showBLETimeTrustDialog = true;
467  /// Whether to warn the user when the time the bluetooth device reports is far
468  /// in the past and [trustBLETime] is true.
469  bool get showBLETimeTrustDialog => _showBLETimeTrustDialog;
470  set showBLETimeTrustDialog(bool value) {
471    _showBLETimeTrustDialog = value;
472    notifyListeners();
473  }
474// When adding fields notice the checklist at the top.
475}
476
477/// Extension to add a serialize method that can be restored by
478/// [ConvertUtil.parseThemeMode].
479extension Serialization on ThemeMode {
480  /// Turns enum into a restoreable integer.
481  int serialize() => switch(this) {
482    ThemeMode.system =>  0,
483    ThemeMode.dark => 1,
484    ThemeMode.light => 2,
485  };
486}