Commit 02692da

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2023-10-07 21:52:30
add screen to delete all data
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent cf3c17d
lib/components/settings_widgets.dart
@@ -22,6 +22,7 @@ class SettingsTile extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    // TODO: use Proper disabled widget, convert to ListTile
     if (disabled) return const SizedBox.shrink();
 
     var lead = SizedBox(
@@ -29,7 +30,6 @@ class SettingsTile extends StatelessWidget {
       child: leading ?? const SizedBox.shrink(),
     );
     var trail = trailing ?? const SizedBox.shrink();
-
     return InkWell(
       onTap: () => onPressed(context),
       child: ConstrainedBox(
lib/l10n/app_en.arb
@@ -425,5 +425,19 @@
   "requiresAppRestart": "Requires app restart",
   "@requiresAppRestart": {},
   "pleaseRestart": "Please restart the app to apply changes",
-  "@pleaseRestart": {}
+  "@pleaseRestart": {},
+  "restartNow": "Restart now",
+  "@restartNow": {},
+  "warnNeedsRestartForUsingApp": "Files where deleted this session. Restart the app to continue using returning to other parts of the app!",
+  "@warnNeedsRestartForUsingApp": {},
+  "deleteAllMeasurements": "Delete all measurements",
+  "@deleteAllMeasurements": {},
+  "deleteAllSettings": "Delete all settings",
+  "@deleteAllSettings": {},
+  "warnDeletionUnrecoverable": "This step not revertible unless you manually made a backup. Do you really want to delete this?",
+  "@warnDeletionUnrecoverable": {},
+  "fileDeleted": "File deleted",
+  "@fileDeleted": {},
+  "fileAlreadyDeleted": "File already deleted",
+  "@fileAlreadyDeleted": {}
 }
lib/model/storage/db/config_dao.dart
@@ -53,6 +53,7 @@ class ConfigDao {
   ///
   /// Adds an entry if no settings where saved for this profile.
   Future<void> _updateSettings(int profileID, Settings settings) async {
+    if (!_configDB.database.isOpen) return;
     await _configDB.database.insert(
       ConfigDB.settingsTable,
       {
@@ -99,6 +100,7 @@ class ConfigDao {
   ///
   /// Adds an entry if necessary.
   Future<void> _updateExportSettings(int profileID, ExportSettings settings) async {
+    if (!_configDB.database.isOpen) return;
     await _configDB.database.insert(
         ConfigDB.exportSettingsTable,
         {
@@ -145,6 +147,7 @@ class ConfigDao {
   ///
   /// Adds an entry if necessary.
   Future<void> _updateCsvExportSettings(int profileID, CsvExportSettings settings) async {
+    if (!_configDB.database.isOpen) return;
     await _configDB.database.insert(
         ConfigDB.exportCsvSettingsTable,
         {
@@ -191,6 +194,7 @@ class ConfigDao {
   ///
   /// Adds an entry if necessary.
   Future<void> _updatePdfExportSettings(int profileID, PdfExportSettings settings) async {
+    if (!_configDB.database.isOpen) return;
     await _configDB.database.insert(
         ConfigDB.exportPdfSettingsTable,
         {
@@ -237,6 +241,7 @@ class ConfigDao {
   ///
   /// Adds an entry if necessary.
   Future<void> _updateIntervallStorage(int profileID, int storageID, IntervallStorage intervallStorage) async {
+    if (!_configDB.database.isOpen) return;
     final Map<String, dynamic> columnValueMap = {
       'profile_id': profileID,
       'storage_id': storageID,
@@ -272,6 +277,7 @@ class ConfigDao {
   ///
   /// If one with the same [ExportColumn.internalName] exists, it will get replaced by the new one regardless of content.
   Future<void> updateExportColumn(ExportColumn exportColumn) async {
+    if (!_configDB.database.isOpen) return;
     await _configDB.database.insert(
         ConfigDB.exportStringsTable,
         {
@@ -285,6 +291,7 @@ class ConfigDao {
 
   /// Deletes the [ExportColumn] where [ExportColumn.internalName] matches [internalName] from the database.
   Future<void> deleteExportColumn(String internalName) async {
+    if (!_configDB.database.isOpen) return;
     await _configDB.database.delete('exportStrings', where: 'internalColumnName = ?', whereArgs: [internalName]);
   }
 }
\ No newline at end of file
lib/model/storage/intervall_store.dart
@@ -262,6 +262,14 @@ class IntervallStoreManager extends ChangeNotifier {
   IntervallStorage mainPage;
   IntervallStorage exportPage;
   IntervallStorage statsPage;
+
+  @override
+  void dispose() {
+    super.dispose();
+    mainPage.dispose();
+    exportPage.dispose();
+    statsPage.dispose();
+  }
 }
 
 /// enum of all locations supported by IntervallStoreManager
lib/model/blood_pressure.dart
@@ -78,6 +78,7 @@ class BloodPressureModel extends ChangeNotifier {
 
   /// Adds a new measurement at the correct chronological position in the List.
   Future<void> add(BloodPressureRecord measurement) async {
+    if (!_database.isOpen) return;
     final existing = await _database.query('bloodPressureModel',
         where: 'timestamp = ?', whereArgs: [measurement.creationTime.millisecondsSinceEpoch]);
     if (existing.isNotEmpty) {
@@ -106,12 +107,14 @@ class BloodPressureModel extends ChangeNotifier {
   }
 
   Future<void> delete(DateTime timestamp) async {
+    if (!_database.isOpen) return;
     _database.delete('bloodPressureModel', where: 'timestamp = ?', whereArgs: [timestamp.millisecondsSinceEpoch]);
     notifyListeners();
   }
 
   /// Returns all recordings in saved in a range in ascending order
   Future<UnmodifiableListView<BloodPressureRecord>> getInTimeRange(DateTime from, DateTime to) async {
+    if (!_database.isOpen) return UnmodifiableListView([]);
     final dbEntries = await _database.query('bloodPressureModel',
         orderBy: 'timestamp DESC',
         where: 'timestamp BETWEEN ? AND ?',
@@ -121,11 +124,12 @@ class BloodPressureModel extends ChangeNotifier {
   }
 
   Future<UnmodifiableListView<BloodPressureRecord>> get all async {
+    if (!_database.isOpen) return UnmodifiableListView([]);
     return UnmodifiableListView(_convert(await _database.query('bloodPressureModel', columns: ['*'])));
   }
 
-  void close() {
-    _database.close();
+  Future<void> close() {
+    return _database.close();
   }
 
   List<BloodPressureRecord> _convert(List<Map<String, Object?>> dbResult) {
lib/model/ram_only_implementations.dart
@@ -40,5 +40,5 @@ class RamBloodPressureModel extends ChangeNotifier implements BloodPressureModel
   Future<UnmodifiableListView<BloodPressureRecord>> get all async => UnmodifiableListView(_records);
 
   @override
-  void close() {}
+  Future<void> close() async {}
 }
\ No newline at end of file
lib/screens/subsettings/delete_data.dart
@@ -0,0 +1,232 @@
+import 'dart:io';
+import 'dart:math';
+
+import 'package:blood_pressure_app/components/consistent_future_builder.dart';
+import 'package:blood_pressure_app/main.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:path/path.dart';
+import 'package:restart_app/restart_app.dart';
+import 'package:sqflite/sqflite.dart';
+
+class DeleteDataScreen extends StatefulWidget {
+  const DeleteDataScreen({super.key});
+
+  @override
+  State<DeleteDataScreen> createState() => _DeleteDataScreenState();
+}
+
+class _DeleteDataScreenState extends State<DeleteDataScreen> {
+  /// Whether or not files were deleted while on this page.
+  ///
+  /// Should never be reset to false.
+  bool _deletedData = false;
+
+  // TODO: localize texts
+  @override
+  Widget build(BuildContext context) {
+    final localizations = AppLocalizations.of(context)!;
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Delete data'),
+        leading: IconButton(
+          icon: Icon(Icons.arrow_back),
+          onPressed: () {
+            if (_deletedData) {
+              Restart.restartApp();
+            } else {
+              Navigator.of(context).pop();
+            }
+          },
+        ),
+      ),
+      body: Column(
+        children: [
+          if (_deletedData)
+            MaterialBanner(
+                content: Text(localizations.warnNeedsRestartForUsingApp),
+                actions: [
+                  TextButton(onPressed: () {
+                    Restart.restartApp();
+                  }, child: Text(localizations.restartNow))
+                ]
+            ),
+          Expanded(
+            child: Container(
+              padding: const EdgeInsets.all(10),
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(localizations.data, style: Theme.of(context).textTheme.headlineMedium),
+                  Expanded(
+                    flex: 1,
+                    child: ListView(
+                      children: [
+                        ListTile(
+                          leading: const Icon(Icons.timeline),
+                          title: Text(localizations.deleteAllMeasurements),
+                          subtitle: ConsistentFutureBuilder(
+                            future: Future(() async {
+                              final String dbPath = join(await getDatabasesPath(), 'blood_pressure.db');
+                              final String dbJournalPath = join(await getDatabasesPath(), 'blood_pressure.db-journal');
+                              int sizeBytes;
+                              try {
+                                sizeBytes = File(dbPath).lengthSync();
+                              } on PathNotFoundException {
+                                sizeBytes = 0;
+                              }
+                              try {
+                                sizeBytes += File(dbJournalPath).lengthSync();
+                              } on PathNotFoundException {}
+
+                              return _bytesToString(sizeBytes);
+                            }),
+                            onData: (context, data) {
+                              return Text(data.toString());
+                            },
+                          ),
+                          trailing: const Icon(Icons.delete_forever),
+                          onTap: () async {
+                            final messanger = ScaffoldMessenger.of(context);
+                            if (await showDeleteDialoge(context, localizations)) {
+                            final String dbPath = join(await getDatabasesPath(), 'blood_pressure.db');
+                            final String dbJournalPath = join(await getDatabasesPath(), 'blood_pressure.db-journal');
+                            await closeDatabases();
+                            tryDeleteFile(dbPath, messanger, localizations);
+                            tryDeleteFile(dbJournalPath, messanger, localizations);
+                            setState(() {
+                            _deletedData = true;
+                            });
+                            }
+                          },
+                        ),
+                        ListTile(
+                          leading: const Icon(Icons.settings),
+                          title: Text(localizations.deleteAllSettings),
+                          subtitle: ConsistentFutureBuilder(
+                            future: Future(() async {
+                              final String dbPath = join(await getDatabasesPath(), 'config.db');
+                              final String dbJournalPath = join(await getDatabasesPath(), 'config.db-journal');
+                              int sizeBytes;
+                              try {
+                                sizeBytes = File(dbPath).lengthSync();
+                              } on PathNotFoundException {
+                                sizeBytes = 0;
+                              }
+                              try {
+                                sizeBytes += File(dbJournalPath).lengthSync();
+                              } on PathNotFoundException {}
+                              return _bytesToString(sizeBytes);
+                            }),
+                            onData: (context, data) {
+                              return Text(data.toString());
+                            },
+                          ),
+                          trailing: const Icon(Icons.delete_forever),
+                          onTap: () async {
+                            final messanger = ScaffoldMessenger.of(context);
+                            if (await showDeleteDialoge(context, localizations)) {
+                              final String dbPath = join(await getDatabasesPath(), 'config.db');
+                              final String dbJournalPath = join(await getDatabasesPath(), 'config.db-journal');
+                              await closeDatabases();
+                              tryDeleteFile(dbPath, messanger, localizations);
+                              tryDeleteFile(dbJournalPath, messanger, localizations);
+                              setState(() {
+                                _deletedData = true;
+                              });
+                            }
+                          },
+                        )
+                      ],
+                    ),
+                  ),
+                  /* Text('Files', style: Theme.of(context).textTheme.headlineMedium),
+                  Expanded(
+                    flex: 3,
+                    child: ConsistentFutureBuilder(
+                      future: Future(() async => Directory(await getDatabasesPath()).list(recursive: true).toList()),
+                      onData: (context, files) =>
+                        ListView.builder(
+                          itemCount: files.length,
+                          itemBuilder: (context, idx) => ListTile(
+                            title: Text(files[idx].path),
+                            trailing: const Icon(Icons.delete_forever),
+                            onTap: () async {
+                              final messanger = ScaffoldMessenger.of(context);
+                              if (await showDeleteDialoge(context, localizations)) {
+                                if (!context.mounted) return;
+                                await unregisterAllProviders(context);
+                                files[idx].deleteSync();
+                                messanger.showSnackBar(SnackBar(
+                                  duration: const Duration(seconds: 5),
+                                  content: Text('File deleted.'),
+                                ));
+                                setState(() {
+                                  _deletedData = true;
+                                });
+                              }
+                            },
+                          )
+                        )
+                    ),
+                  ), */
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// Converts file size in bytes to human readable string
+  String _bytesToString(int sizeBytes) {
+    if (sizeBytes <= 0) return "0 B";
+    const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+    var i = (log(sizeBytes) / log(1024)).floor();
+    return '${(sizeBytes / pow(1024, i)).toStringAsFixed(1)} ${suffixes[i]}';
+  }
+
+  Future<bool> showDeleteDialoge(BuildContext context, AppLocalizations localizations) async {
+    return await showDialog<bool>(context: context, builder: (context) =>
+        AlertDialog(
+          title: Text(localizations.confirmDelete),
+          content: Text(localizations.warnDeletionUnrecoverable),
+          actionsAlignment: MainAxisAlignment.spaceBetween,
+          actions: [
+            TextButton(
+                onPressed: () => Navigator.of(context).pop(false),
+                child: Text(AppLocalizations.of(context)!.btnCancel)),
+            Theme(
+              data: ThemeData.from(
+                  colorScheme: ColorScheme.fromSeed(seedColor: Colors.red, brightness: Theme.of(context).brightness),
+                  useMaterial3: true
+              ),
+              child: ElevatedButton.icon(
+                  onPressed: () => Navigator.of(context).pop(true),
+                  icon: const Icon(Icons.delete_forever),
+                  label: Text(AppLocalizations.of(context)!.btnConfirm)
+              ),
+            )
+
+          ],
+        )
+    ) ?? false;
+  }
+  
+  void tryDeleteFile(String path, ScaffoldMessengerState messanger, AppLocalizations localizations) {
+    try {
+      File(path).deleteSync();
+      messanger.showSnackBar(SnackBar(
+        duration: const Duration(seconds: 2),
+        content: Text(localizations.fileDeleted),
+      ));
+    } on PathNotFoundException {
+      messanger.showSnackBar(SnackBar(
+        duration: const Duration(seconds: 2),
+        content: Text(localizations.fileAlreadyDeleted),
+      ));
+    }
+  }
+}
\ No newline at end of file
lib/screens/settings.dart
@@ -7,6 +7,7 @@ import 'package:blood_pressure_app/model/blood_pressure.dart';
 import 'package:blood_pressure_app/model/iso_lang_names.dart';
 import 'package:blood_pressure_app/model/settings_store.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:blood_pressure_app/screens/subsettings/delete_data.dart';
 import 'package:blood_pressure_app/screens/subsettings/enter_timeformat.dart';
 import 'package:blood_pressure_app/screens/subsettings/export_import_screen.dart';
 import 'package:blood_pressure_app/screens/subsettings/graph_markings.dart';
@@ -337,6 +338,17 @@ class SettingsPage extends StatelessWidget {
                       }
                     }
                 ),
+                SettingsTile(
+                    title: Text(localizations.delete),
+                    leading: const Icon(Icons.delete),
+                    trailing: const Icon(Icons.arrow_forward_ios),
+                    onPressed: (context) {
+                      Navigator.push(
+                        context,
+                        MaterialPageRoute(builder: (context) => const DeleteDataScreen()),
+                      );
+                    }
+                )
               ],
             ),
             SettingsSection(title: Text(localizations.aboutWarnValuesScreen), children: [
@@ -344,6 +356,7 @@ class SettingsPage extends StatelessWidget {
                   key: const Key('version'),
                   title: Text(localizations.version),
                   leading: const Icon(Icons.info_outline),
+                  trailing: const Icon(Icons.arrow_forward_ios),
                   description: ConsistentFutureBuilder<PackageInfo>(
                     future: PackageInfo.fromPlatform(),
                     onData: (context, info) => Text(info.version)
lib/main.dart
@@ -15,13 +15,16 @@ late AppLocalizations gLocalizations;
 @Deprecated('This should not be used for new code, but rather for migrating existing code.')
 late final ConfigDao globalConfigDao;
 
+late final ConfigDB _database;
+late final BloodPressureModel _bloodPressureModel;
+
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
   // 2 different db files
-  final dataModel = await BloodPressureModel.create();
+  _bloodPressureModel = await BloodPressureModel.create();
 
-  final configDB = await ConfigDB.open();
-  final configDao = ConfigDao(configDB);
+  _database = await ConfigDB.open();
+  final configDao = ConfigDao(_database);
 
   final settings = await configDao.loadSettings(0);
   final exportSettings = await configDao.loadExportSettings(0);
@@ -37,7 +40,7 @@ void main() async {
   intervalStorageManager.mainPage.setToMostRecentIntervall();
 
   runApp(MultiProvider(providers: [
-    ChangeNotifierProvider(create: (context) => dataModel),
+    ChangeNotifierProvider(create: (context) => _bloodPressureModel),
     ChangeNotifierProvider(create: (context) => settings),
     ChangeNotifierProvider(create: (context) => exportSettings),
     ChangeNotifierProvider(create: (context) => csvExportSettings),
@@ -99,3 +102,17 @@ class AppRoot extends StatelessWidget {
     }
   }
 }
+
+bool _areAllProvidersUnregistered = false;
+/// Close all connections to the databases and remove all listeners from provided objects.
+///
+/// The app will most likely stop working after invoking this.
+///
+/// Invoking the function multiple times is safe.
+Future<void> closeDatabases() async {
+  if (_areAllProvidersUnregistered) return;
+  _areAllProvidersUnregistered = true;
+
+  await _database.database.close();
+  await _bloodPressureModel.close();
+}