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}