main
1import 'dart:io';
2
3import 'package:blood_pressure_app/data_util/consistent_future_builder.dart';
4import 'package:blood_pressure_app/l10n/app_localizations.dart';
5import 'package:blood_pressure_app/model/storage/db/file_settings_loader.dart';
6import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
7import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
8import 'package:blood_pressure_app/model/storage/export_xsl_settings_store.dart';
9import 'package:blood_pressure_app/model/storage/storage.dart';
10import 'package:blood_pressure_app/screens/error_reporting_screen.dart';
11import 'package:blood_pressure_app/screens/home_screen.dart';
12import 'package:blood_pressure_app/screens/loading_screen.dart';
13import 'package:flutter/foundation.dart';
14import 'package:flutter/material.dart';
15import 'package:flutter_bloc/flutter_bloc.dart';
16import 'package:health_data_store/health_data_store.dart';
17import 'package:package_info_plus/package_info_plus.dart';
18import 'package:path/path.dart';
19import 'package:provider/provider.dart';
20import 'package:sqflite_common_ffi/sqflite_ffi.dart';
21
22/// Base class for the entire app.
23///
24/// Sets up databases, performs update logic and provides styles and ancestors
25/// that should be available everywhere in the app.
26class App extends StatefulWidget {
27 /// Create the base for the entire app.
28 const App();
29
30 @override
31 State<App> createState() => _AppState();
32}
33
34class _AppState extends State<App> {
35 Database? _entryDB;
36
37 /// The result of the first [_loadApp] call.
38 ///
39 /// Storing this is necessary to ensure the app is not loaded multiple times.
40 Widget? _loadedChild;
41 Settings? _settings;
42 ExportSettings? _exportSettings;
43 CsvExportSettings? _csvExportSettings;
44 PdfExportSettings? _pdfExportSettings;
45 ExcelExportSettings? _xslExportSettings;
46 IntervalStoreManager? _intervalStorageManager;
47 ExportColumnsManager? _exportColumnsManager;
48
49 @override
50 void dispose() {
51 _entryDB?.close();
52 _entryDB = null;
53 _settings?.dispose();
54 _exportSettings?.dispose();
55 _csvExportSettings?.dispose();
56 _pdfExportSettings?.dispose();
57 _xslExportSettings?.dispose();
58 _intervalStorageManager?.dispose();
59 _exportColumnsManager?.dispose();
60 super.dispose();
61 }
62
63 /// Load the primary app data asynchronously to allow load animations.
64 Future<Widget> _loadApp() async {
65 if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
66 databaseFactory = databaseFactoryFfi;
67 }
68
69 if (kDebugMode && (const bool.fromEnvironment('testing_mode'))) {
70 final dbPath = await getDatabasesPath();
71 try {
72 File(join(dbPath, 'bp.db')).deleteSync();
73 File(join(dbPath, 'bp.db-journal')).deleteSync();
74 } on FileSystemException {
75 // File is likely already deleted or couldn't be created in the first place.
76 }
77 try {
78 File(join(dbPath, 'config.db')).deleteSync();
79 File(join(dbPath, 'config.db-journal')).deleteSync();
80 } on FileSystemException {
81 // No file to delete
82 }
83 try {
84 File(join(dbPath, 'medicine.intakes')).deleteSync();
85 } on FileSystemException {
86 // No file to delete
87 }
88 }
89
90 try {
91 final SettingsLoader settingsLoader = await FileSettingsLoader.load();
92 _settings ??= await settingsLoader.loadSettings();
93 _exportSettings ??= await settingsLoader.loadExportSettings();
94 _csvExportSettings ??= await settingsLoader.loadCsvExportSettings();
95 _pdfExportSettings ??= await settingsLoader.loadPdfExportSettings();
96 _xslExportSettings ??= await settingsLoader.loadXslExportSettings();
97 _intervalStorageManager ??= await settingsLoader.loadIntervalStorageManager();
98 _exportColumnsManager ??= await settingsLoader.loadExportColumnsManager();
99 } catch (e, stack) {
100 await ErrorReporting.reportCriticalError('Error loading settings from files', '$e\n$stack',);
101 }
102
103 late BloodPressureRepository bpRepo;
104 late NoteRepository noteRepo;
105 late MedicineRepository medRepo;
106 late MedicineIntakeRepository intakeRepo;
107 late BodyweightRepository weightRepo;
108
109 try {
110 _entryDB = await openDatabase(
111 join(await getDatabasesPath(), 'bp.db'),
112 );
113 final db = await HealthDataStore.load(_entryDB!);
114 bpRepo = db.bpRepo;
115 noteRepo = db.noteRepo;
116 medRepo = db.medRepo;
117 intakeRepo = db.intakeRepo;
118 weightRepo = db.weightRepo;
119 } catch (e, stack) {
120 await ErrorReporting.reportCriticalError('Error loading entry db', '$e\n$stack',);
121 }
122
123 try {
124 // update logic
125 if (_settings!.allowMissingValues && _settings!.validateInputs){
126 _settings!.validateInputs = false;
127 }
128
129 _settings!.lastVersion = int.parse((await PackageInfo.fromPlatform()).buildNumber);
130
131 // Reset the step size interval to current on startup
132 _intervalStorageManager!.mainPage.setToMostRecentInterval();
133 } catch (e, stack) {
134 await ErrorReporting.reportCriticalError('Error performing upgrades:', '$e\n$stack',);
135 }
136
137 final dbPath = await getDatabasesPath();
138 if (File(join(dbPath, 'config.db')).existsSync()) {
139 try {
140 await migrateDatabaseSettings(
141 _settings!,
142 _exportSettings!,
143 _csvExportSettings!,
144 _pdfExportSettings!,
145 _intervalStorageManager!,
146 _exportColumnsManager!,
147 medRepo,
148 );
149 File(join(dbPath, 'config.db')).copySync(join(dbPath, 'v39_config.db.backup'));
150 File(join(dbPath, 'config.db')).deleteSync();
151 if (File(join(dbPath, 'config.db-journal')).existsSync()) {
152 File(join(dbPath, 'config.db-journal')).copySync(join(dbPath, 'v39_config.db-journal.backup'));
153 File(join(dbPath, 'config.db-journal')).deleteSync();
154 }
155 } catch (e, stack) {
156 await ErrorReporting.reportCriticalError('Error upgrading to file based settings:', '$e\n$stack',);
157 }
158 }
159
160 _loadedChild = MultiRepositoryProvider(
161 providers: [
162 RepositoryProvider.value(value: bpRepo),
163 RepositoryProvider.value(value: noteRepo),
164 RepositoryProvider.value(value: medRepo),
165 RepositoryProvider.value(value: intakeRepo),
166 RepositoryProvider.value(value: weightRepo),
167 ],
168 child: MultiProvider(
169 providers: [
170 ChangeNotifierProvider.value(value: _settings!),
171 ChangeNotifierProvider.value(value: _exportSettings!),
172 ChangeNotifierProvider.value(value: _csvExportSettings!),
173 ChangeNotifierProvider.value(value: _pdfExportSettings!),
174 ChangeNotifierProvider.value(value: _xslExportSettings!),
175 ChangeNotifierProvider.value(value: _intervalStorageManager!),
176 ChangeNotifierProvider.value(value: _exportColumnsManager!),
177 ],
178 child: _buildAppRoot(),
179 ),
180 );
181
182 return _loadedChild!;
183 }
184
185 @override
186 Widget build(BuildContext context) {
187 if (!(kDebugMode && (const bool.fromEnvironment('testing_mode')))
188 && _loadedChild != null && _settings != null && _entryDB != null) {
189 return _loadedChild!;
190 }
191 return ConsistentFutureBuilder(
192 future: _loadApp(),
193 onWaiting: const LoadingScreen(),
194 onData: (context, widget) => widget,
195 );
196 }
197
198 /// Central [MaterialApp] widget of the app that sets the uniform style options.
199 Widget _buildAppRoot() => Consumer<Settings>(
200 builder: (context, settings, child) => MaterialApp(
201 title: 'Blood Pressure App',
202 onGenerateTitle: (context) => AppLocalizations.of(context)!.title,
203 theme: _buildTheme(ColorScheme.fromSeed(
204 seedColor: settings.accentColor,
205 ),),
206 darkTheme: _buildTheme(ColorScheme.fromSeed(
207 seedColor: settings.accentColor,
208 brightness: Brightness.dark,
209 ),),
210 themeMode: settings.themeMode,
211 localizationsDelegates: AppLocalizations.localizationsDelegates,
212 supportedLocales: AppLocalizations.supportedLocales,
213 locale: settings.language,
214 debugShowCheckedModeBanner: false,
215 home: const AppHome(),
216 ),
217 );
218
219 ThemeData _buildTheme(ColorScheme colorScheme) {
220 final inputBorder = OutlineInputBorder(
221 borderSide: BorderSide(
222 width: 3,
223 // Through black background outlineVariant has enough contrast.
224 color: (colorScheme.brightness == Brightness.dark)
225 ? colorScheme.outlineVariant
226 : colorScheme.outline,
227 ),
228 borderRadius: BorderRadius.circular(20),
229 );
230
231 return ThemeData(
232 colorScheme: colorScheme,
233 useMaterial3: true,
234 inputDecorationTheme: InputDecorationTheme(
235 errorMaxLines: 5,
236 border: inputBorder,
237 enabledBorder: inputBorder,
238 ),
239 scaffoldBackgroundColor: colorScheme.brightness == Brightness.dark
240 ? Colors.black
241 : Colors.white,
242 appBarTheme: const AppBarTheme(
243 centerTitle: true,
244 shape: RoundedRectangleBorder(
245 borderRadius: BorderRadius.only(
246 bottomRight: Radius.circular(15),
247 bottomLeft: Radius.circular(15),
248 ),
249 ),
250 ),
251 snackBarTheme: SnackBarThemeData(
252 shape: RoundedRectangleBorder(
253 borderRadius: BorderRadius.circular(8),
254 ),
255 ),
256 );
257 }
258}