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}