Commit 02692da
Changed files (9)
lib
components
l10n
screens
subsettings
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();
+}