Commit d084c3f
Changed files (20)
app
lib
l10n
model
screens
test
model
app/lib/l10n/app_en.arb
@@ -516,5 +516,9 @@
"intakes": "Medicine intakes",
"@intakes": {},
"errFeatureNotSupported": "This feature is not available on this platform.",
- "@errFeatureNotSupported": {}
+ "@errFeatureNotSupported": {},
+ "invalidZip": "Invalid zip file.",
+ "@invalidZip": {},
+ "errCantCreateArchive": "Can''t create archive. Please report the bug if possible.",
+ "@errCantCreateArchive": {}
}
\ No newline at end of file
app/lib/model/storage/db/config_dao.dart
@@ -1,42 +1,37 @@
import 'package:blood_pressure_app/model/storage/db/config_db.dart';
+import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:sqflite/sqflite.dart';
/// Class for loading data from the database.
///
-/// The user of this class needs to pay attention to dispose all old instances
+/// The user of this class needs to pay attention to dispose all old instance
/// of objects created by the instance methods in order to ensure there are no
-/// concurrent writes to the database. Having multiple instances will cause data
+/// concurrent writes to the database. Having multiple instance will cause data
/// loss because states are not synced again after one changes.
///
/// The load... methods have to schedule a initial save to db in case an
/// migration / update of fields occurred.
-class ConfigDao {
+@deprecated
+class ConfigDao implements SettingsLoader {
/// Create a serializer to initialize data from a database.
ConfigDao(this._configDB);
final ConfigDB _configDB;
- final Map<int, Settings> _settingsInstances = {};
- /// Loads the profiles [Settings] object from the database.
- ///
- /// If any errors occur or the object is not present, a default one will be
- /// created. Changes in the object will save to the database automatically (a
- /// listener gets attached).
- ///
- /// Changes to the database will not propagate to the object.
- Future<Settings> loadSettings(int profileID) async {
- if (_settingsInstances.containsKey(profileID)) return _settingsInstances[profileID]!;
+ Settings? _settingsInstance = null;
+ @override
+ Future<Settings> loadSettings() async {
+ if (_settingsInstance != null) return _settingsInstance!;
final dbEntry = await _configDB.database.query(
ConfigDB.settingsTable,
columns: ['settings_json'],
where: 'profile_id = ?',
- whereArgs: [profileID],
+ whereArgs: [0],
);
late final Settings settings;
@@ -51,43 +46,19 @@ class ConfigDao {
settings = Settings.fromJson(settingsJson.toString());
}
}
- _updateSettings(profileID, settings);
- settings.addListener(() {
- _updateSettings(profileID, settings);
- });
- _settingsInstances[profileID] = settings;
+ _settingsInstance = settings;
return settings;
}
- /// Update settings for a profile in the database.
- ///
- /// 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,
- {
- 'profile_id': profileID,
- 'settings_json': settings.toJson(),
- },
- conflictAlgorithm: ConflictAlgorithm.replace,
- );
- }
-
- final Map<int, ExportSettings> _exportSettingsInstances = {};
- /// Loads the profiles [ExportSettings] object from the database.
- ///
- /// If any errors occur or the object is not present, a default one will be created. Changes in the object
- /// will save to the database automatically (a listener gets attached).
- ///
- /// Changes to the database will not propagate to the object.
- Future<ExportSettings> loadExportSettings(int profileID) async {
- if (_exportSettingsInstances.containsKey(profileID)) return _exportSettingsInstances[profileID]!;
+ ExportSettings? _exportSettingsInstance = null;
+ @override
+ Future<ExportSettings> loadExportSettings() async {
+ if (_exportSettingsInstance != null) return _exportSettingsInstance!;
final dbEntry = await _configDB.database.query(
ConfigDB.exportSettingsTable,
columns: ['json'],
where: 'profile_id = ?',
- whereArgs: [profileID],
+ whereArgs: [0],
);
late final ExportSettings exportSettings;
@@ -102,44 +73,19 @@ class ConfigDao {
exportSettings = ExportSettings.fromJson(settingsJson.toString());
}
}
- _updateExportSettings(profileID, exportSettings);
- exportSettings.addListener(() {
- _updateExportSettings(profileID, exportSettings);
- });
- _exportSettingsInstances[profileID] = exportSettings;
+ _exportSettingsInstance = exportSettings;
return exportSettings;
}
- /// Update [ExportSettings] for a profile in the database.
- ///
- /// Adds an entry if necessary.
- Future<void> _updateExportSettings(int profileID, ExportSettings settings) async {
- if (!_configDB.database.isOpen) return;
- await _configDB.database.insert(
- ConfigDB.exportSettingsTable,
- {
- 'profile_id': profileID,
- 'json': settings.toJson(),
- },
- conflictAlgorithm: ConflictAlgorithm.replace,
- );
- }
-
- final Map<int, CsvExportSettings> _csvExportSettingsInstances = {};
-
- /// Loads the profiles [CsvExportSettings] object from the database.
- ///
- /// If any errors occur or the object is not present, a default one will be created. Changes in the object
- /// will save to the database automatically (a listener gets attached).
- ///
- /// Changes to the database will not propagate to the object.
- Future<CsvExportSettings> loadCsvExportSettings(int profileID) async {
- if (_csvExportSettingsInstances.containsKey(profileID)) return _csvExportSettingsInstances[profileID]!;
+ CsvExportSettings? _csvExportSettingsInstance = null;
+ @override
+ Future<CsvExportSettings> loadCsvExportSettings() async {
+ if (_csvExportSettingsInstance != null) return _csvExportSettingsInstance!;
final dbEntry = await _configDB.database.query(
ConfigDB.exportCsvSettingsTable,
columns: ['json'],
where: 'profile_id = ?',
- whereArgs: [profileID],
+ whereArgs: [0],
);
late final CsvExportSettings exportSettings;
@@ -154,44 +100,19 @@ class ConfigDao {
exportSettings = CsvExportSettings.fromJson(settingsJson.toString());
}
}
- _updateCsvExportSettings(profileID, exportSettings);
- exportSettings.addListener(() {
- _updateCsvExportSettings(profileID, exportSettings);
- });
- _csvExportSettingsInstances[profileID] = exportSettings;
+ _csvExportSettingsInstance = exportSettings;
return exportSettings;
}
- /// Update [CsvExportSettings] for a profile in the database.
- ///
- /// Adds an entry if necessary.
- Future<void> _updateCsvExportSettings(int profileID, CsvExportSettings settings) async {
- if (!_configDB.database.isOpen) return;
- await _configDB.database.insert(
- ConfigDB.exportCsvSettingsTable,
- {
- 'profile_id': profileID,
- 'json': settings.toJson(),
- },
- conflictAlgorithm: ConflictAlgorithm.replace,
- );
- }
-
- final Map<int, PdfExportSettings> _pdfExportSettingsInstances = {};
-
- /// Loads the profiles [PdfExportSettings] object from the database.
- ///
- /// If any errors occur or the object is not present, a default one will be created. Changes in the object
- /// will save to the database automatically (a listener gets attached).
- ///
- /// Changes to the database will not propagate to the object.
- Future<PdfExportSettings> loadPdfExportSettings(int profileID) async {
- if (_pdfExportSettingsInstances.containsKey(profileID)) return _pdfExportSettingsInstances[profileID]!;
+ PdfExportSettings? _pdfExportSettingsInstance = null;
+ @override
+ Future<PdfExportSettings> loadPdfExportSettings() async {
+ if (_pdfExportSettingsInstance != null) return _pdfExportSettingsInstance!;
final dbEntry = await _configDB.database.query(
ConfigDB.exportPdfSettingsTable,
columns: ['json'],
where: 'profile_id = ?',
- whereArgs: [profileID],
+ whereArgs: [0],
);
late final PdfExportSettings exportSettings;
@@ -206,49 +127,27 @@ class ConfigDao {
exportSettings = PdfExportSettings.fromJson(settingsJson.toString());
}
}
- _updatePdfExportSettings(profileID, exportSettings);
- exportSettings.addListener(() {
- _updatePdfExportSettings(profileID, exportSettings);
- });
- _pdfExportSettingsInstances[profileID] = exportSettings;
+ _pdfExportSettingsInstance = exportSettings;
return exportSettings;
}
- /// Update [PdfExportSettings] for a profile in the database.
- ///
- /// Adds an entry if necessary.
- Future<void> _updatePdfExportSettings(int profileID, PdfExportSettings settings) async {
- if (!_configDB.database.isOpen) return;
- await _configDB.database.insert(
- ConfigDB.exportPdfSettingsTable,
- {
- 'profile_id': profileID,
- 'json': settings.toJson(),
- },
- conflictAlgorithm: ConflictAlgorithm.replace,
+ IntervalStoreManager? _intervallStorageInstance;
+ @override
+ Future<IntervalStoreManager> loadIntervalStorageManager() async {
+ _intervallStorageInstance ??= IntervalStoreManager(
+ await _loadStore(0),
+ await _loadStore(1),
+ await _loadStore(2),
);
+ return _intervallStorageInstance!;
}
- final Map<(int, int), IntervalStorage> _intervallStorageInstances = {};
-
- /// Loads a [IntervalStorage] object of a [profileID] from the database.
- ///
- /// The [storageID] allows for associating multiple intervalls with one profile.
- ///
- /// If any errors occur or the object is not present, a default one will be created. Changes in the object
- /// will save to the database automatically (a listener gets attached).
- ///
- /// Changes to the database will not propagate to the object.
- ///
- /// This should not be invoked directly in order to centralise [storageID] allocation. Currently this is done by
- /// the [IntervalStoreManager] class.
- Future<IntervalStorage> loadIntervalStorage(int profileID, int storageID) async {
- if (_intervallStorageInstances.containsKey((profileID, storageID))) return _intervallStorageInstances[(profileID, storageID)]!;
+ Future<IntervalStorage> _loadStore(int storageID) async {
final dbEntry = await _configDB.database.query(
- ConfigDB.selectedIntervalStorageTable,
- columns: ['stepSize', 'start', 'end'],
- where: 'profile_id = ? AND storage_id = ?',
- whereArgs: [profileID, storageID],
+ ConfigDB.selectedIntervalStorageTable,
+ columns: ['stepSize', 'start', 'end'],
+ where: 'profile_id = ? AND storage_id = ?',
+ whereArgs: [0, storageID],
);
late final IntervalStorage intervallStorage;
if (dbEntry.isEmpty) {
@@ -257,47 +156,18 @@ class ConfigDao {
assert(dbEntry.length == 1, 'Keys should ensure only one entry is possible.');
intervallStorage = IntervalStorage.fromMap(dbEntry.first);
}
-
- _updateIntervallStorage(profileID, storageID, intervallStorage);
- intervallStorage.addListener(() {
- _updateIntervallStorage(profileID, storageID, intervallStorage);
- });
- _intervallStorageInstances[(profileID, storageID)] = intervallStorage;
return intervallStorage;
}
- /// Update specific [IntervalStorage] for a profile in the database.
- ///
- /// Adds an entry if necessary.
- Future<void> _updateIntervallStorage(int profileID, int storageID, IntervalStorage intervallStorage) async {
- if (!_configDB.database.isOpen) return;
- final Map<String, dynamic> columnValueMap = {
- 'profile_id': profileID,
- 'storage_id': storageID,
- };
- columnValueMap.addAll(intervallStorage.toMap());
- await _configDB.database.insert(
- ConfigDB.selectedIntervalStorageTable,
- columnValueMap,
- conflictAlgorithm: ConflictAlgorithm.replace,
- );
- }
-
- final Map<int, ExportColumnsManager> _exportColumnsManagerInstances = {};
-
- /// Loads the profiles [ExportColumnsManager] object from the database.
- ///
- /// If any errors occur or the object is not present, a default one will be created. Changes in the object
- /// will save to the database automatically (a listener gets attached).
- ///
- /// Changes to the database will not propagate to the object.
- Future<ExportColumnsManager> loadExportColumnsManager(int profileID) async {
- if (_exportColumnsManagerInstances.containsKey(profileID)) return _exportColumnsManagerInstances[profileID]!;
+ ExportColumnsManager? _exportColumnsManagerInstance = null;
+ @override
+ Future<ExportColumnsManager> loadExportColumnsManager() async {
+ if (_exportColumnsManagerInstance != null) return _exportColumnsManagerInstance!;
final dbEntry = await _configDB.database.query(
ConfigDB.exportColumnsTable,
columns: ['json'],
where: 'profile_id = ?',
- whereArgs: [profileID],
+ whereArgs: [0],
);
late final ExportColumnsManager columnsManager;
@@ -312,26 +182,7 @@ class ConfigDao {
columnsManager = ExportColumnsManager.fromJson(json.toString());
}
}
- _updateExportColumnsManager(profileID, columnsManager);
- columnsManager.addListener(() {
- _updateExportColumnsManager(profileID, columnsManager);
- });
- _exportColumnsManagerInstances[profileID] = columnsManager;
+ _exportColumnsManagerInstance = columnsManager;
return columnsManager;
}
-
- /// Update [ExportColumnsManager] for a profile in the database.
- ///
- /// Adds an entry if necessary.
- Future<void> _updateExportColumnsManager(int profileID, ExportColumnsManager manager) async {
- if (!_configDB.database.isOpen) return;
- await _configDB.database.insert(
- ConfigDB.exportColumnsTable,
- {
- 'profile_id': profileID,
- 'json': manager.toJson(),
- },
- conflictAlgorithm: ConflictAlgorithm.replace,
- );
- }
}
app/lib/model/storage/db/config_db.dart
@@ -8,6 +8,7 @@ import 'package:sqflite/sqflite.dart';
/// Database for storing settings and internal app state.
///
/// The table names ensured by this class are stored as constant public strings ending in Table.
+@deprecated
class ConfigDB {
ConfigDB._create();
@@ -22,9 +23,6 @@ class ConfigDB {
/// Format:
/// `CREATE TABLE settings(profile_id INTEGER PRIMARY KEY, settings_json STRING)`
static const String settingsTable = 'settings';
- // instead of just changing this string when changing the format, _onDBUpgrade should be used.
- static const String _settingsTableCreationString =
- 'CREATE TABLE settings(profile_id INTEGER PRIMARY KEY, settings_json STRING)';
/// Name of the table for storing [ExportSettings] objects.
///
@@ -33,8 +31,6 @@ class ConfigDB {
/// Format:
/// `CREATE TABLE exportSettings(profile_id INTEGER PRIMARY KEY, json STRING)`
static const String exportSettingsTable = 'exportSettings';
- static const String _exportSettingsTableCreationString =
- 'CREATE TABLE exportSettings(profile_id INTEGER PRIMARY KEY, json STRING)';
/// Name of the table for storing [CsvExportSettings] objects.
///
@@ -43,8 +39,6 @@ class ConfigDB {
/// Format:
/// `CREATE TABLE exportCsvSettings(profile_id INTEGER PRIMARY KEY, json STRING)`
static const String exportCsvSettingsTable = 'exportCsvSettings';
- static const String _exportCsvSettingsTableCreationString =
- 'CREATE TABLE exportCsvSettings(profile_id INTEGER PRIMARY KEY, json STRING)';
/// Name of the table for storing [PdfExportSettings] objects.
///
@@ -53,8 +47,6 @@ class ConfigDB {
/// Format:
/// `CREATE TABLE exportPdfSettings(profile_id INTEGER PRIMARY KEY, json STRING)`
static const String exportPdfSettingsTable = 'exportPdfSettings';
- static const String _exportPdfSettingsTableCreationString =
- 'CREATE TABLE exportPdfSettings(profile_id INTEGER PRIMARY KEY, json STRING)';
/// Name of the table for storing time intervals to display.
///
@@ -68,9 +60,6 @@ class ConfigDB {
/// Format: `CREATE TABLE selectedIntervallStorage(profile_id INTEGER, storage_id INTEGER, stepSize INTEGER,`
/// ` start INTEGER, end INTEGER, PRIMARY KEY(profile_id, storage_id))`
static const String selectedIntervalStorageTable = 'selectedIntervallStorage';
- static const String _selectedIntervalStorageCreationString = 'CREATE TABLE selectedIntervallStorage(profile_id '
- 'INTEGER, storage_id INTEGER, stepSize INTEGER, start INTEGER, end INTEGER, '
- 'PRIMARY KEY(profile_id, storage_id))';
/// Name of the exportStrings table. It is used to to update old columns.
///
@@ -78,8 +67,6 @@ class ConfigDB {
/// `CREATE TABLE exportStrings(internalColumnName STRING PRIMARY KEY, columnTitle STRING, formatPattern STRING)`
@Deprecated('removed after all usages are replaced by export_columns_store')
static const String exportStringsTable = 'exportStrings';
- static const String _exportStringsTableCreationString =
- 'CREATE TABLE exportStrings(internalColumnName STRING PRIMARY KEY, columnTitle STRING, formatPattern STRING)';
/// Name of table of storing [ExportColumnsManager] objects
///
@@ -93,60 +80,43 @@ class ConfigDB {
/// [dbPath] is the path to the folder the database is in. When [dbPath] is left empty the default database file is
/// used. The [isFullPath] option tells the constructor not to add the default filename at the end of [dbPath].
- Future<void> _asyncInit(String? dbPath, bool isFullPath) async {
+ Future<bool> _asyncInit(String? dbPath, bool isFullPath) async {
dbPath ??= await getDatabasesPath();
if (dbPath != inMemoryDatabasePath && !isFullPath) {
dbPath = join(dbPath, 'config.db');
}
-
+ bool dbTooOld = false;
_database = await openDatabase(
dbPath,
- onCreate: _onDBCreate,
- onUpgrade: _onDBUpgrade,
+ onCreate: (_, __) => dbTooOld = true,
+ onUpgrade: (Database db, int oldVersion, int newVersion) async {
+ assert(newVersion == 3);
+ if (oldVersion == 1) {
+ dbTooOld = true;
+ } else if (oldVersion == 2) {
+ await db.execute(_exportColumnsTableCreationString);
+ } else {
+ assert(false, 'Unexpected version upgrade from $oldVersion to $newVersion.');
+ }
+ },
// When increasing the version an update procedure from every other possible version is needed
version: 3,
// In integration tests the file may be deleted which causes deadlocks.
singleInstance: false,
);
- }
-
- FutureOr<void> _onDBCreate(Database db, int version) async {
- await db.execute(_exportStringsTableCreationString);
- await db.execute(_settingsTableCreationString);
- await db.execute(_exportSettingsTableCreationString);
- await db.execute(_exportCsvSettingsTableCreationString);
- await db.execute(_exportPdfSettingsTableCreationString);
- await db.execute(_selectedIntervalStorageCreationString);
- await db.execute(_exportColumnsTableCreationString);
- }
-
- FutureOr<void> _onDBUpgrade(Database db, int oldVersion, int newVersion) async {
- // When adding more versions the upgrade procedure proposed in https://stackoverflow.com/a/75153875/21489239
- // might be useful, to avoid duplicated code. Currently this would only lead to complexity without benefits.
- assert(newVersion == 3);
- if (oldVersion == 1) {
- await db.execute(_settingsTableCreationString);
- await db.execute(_exportSettingsTableCreationString);
- await db.execute(_exportCsvSettingsTableCreationString);
- await db.execute(_exportPdfSettingsTableCreationString);
- await db.execute(_selectedIntervalStorageCreationString);
- await db.execute(_exportColumnsTableCreationString);
- await db.database.setVersion(2);
- } else if (oldVersion == 2) {
- await db.execute(_exportColumnsTableCreationString);
- } else {
- assert(false, 'Unexpected version upgrade from $oldVersion to $newVersion.');
- }
+ return dbTooOld;// TODO: test
}
/// Factory method that either opens an existing db file or creates a new one if no file is present.
///
/// [dbPath] is the path to the folder the database is in. When [dbPath] is left empty the default database file is
/// used. The [isFullPath] option tells the constructor not to add the default filename at the end of [dbPath].
- static Future<ConfigDB> open({String? dbPath, bool isFullPath = false}) async {
+ static Future<ConfigDB?> open({String? dbPath, bool isFullPath = false}) async {
final instance = ConfigDB._create();
- await instance._asyncInit(dbPath, isFullPath);
- return instance;
+ if(await instance._asyncInit(dbPath, isFullPath)) {
+ return instance;
+ }
+ return null;
}
/// The database created by this class for getting and setting entries.
app/lib/model/storage/db/file_settings_loader.dart
@@ -0,0 +1,133 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:archive/archive_io.dart';
+import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
+import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
+import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:flutter/widgets.dart';
+import 'package:path/path.dart';
+import 'package:sqflite/sqflite.dart';
+
+/// Store settings in a directory format on disk.
+class FileSettingsLoader implements SettingsLoader {
+ FileSettingsLoader._create(this._path);
+
+ /// Creates setting loader from relative directory [path] or the default
+ /// settings path.
+ static Future<FileSettingsLoader> load([String? path]) async {
+ path ??= join(await getDatabasesPath(), 'settings');
+ Directory(path).createSync();
+ return FileSettingsLoader._create(path);
+ }
+
+ final String _path;
+
+ /// Instantiates a settings file relative to [_path] and writes changes.
+ ///
+ /// If the is read successfully [build] is called else [createNew] is called.
+ T _loadFile<T extends ChangeNotifier>(
+ String fileName,
+ T Function(String) build,
+ T Function() createNew,
+ String Function(T) serialize,
+ ) {
+ final f = File(join(_path, fileName));
+ T? obj;
+ try {
+ obj = build(f.readAsStringSync());
+ } on FileSystemException {}
+ obj ??= createNew();
+
+ obj.addListener(() => f.writeAsStringSync(serialize(obj!)));
+ f.writeAsStringSync(serialize(obj));
+ return obj;
+ }
+
+ @override
+ Future<CsvExportSettings> loadCsvExportSettings() async => _loadFile(
+ 'csv-export',
+ CsvExportSettings.fromJson,
+ CsvExportSettings.new,
+ (e) => e.toJson(),
+ );
+
+ @override
+ Future<ExportColumnsManager> loadExportColumnsManager() async => _loadFile(
+ 'export-columns',
+ ExportColumnsManager.fromJson,
+ ExportColumnsManager.new,
+ (e) => e.toJson(),
+ );
+
+ @override
+ Future<ExportSettings> loadExportSettings() async => _loadFile(
+ 'export',
+ ExportSettings.fromJson,
+ ExportSettings.new,
+ (e) => e.toJson(),
+ );
+
+ @override
+ Future<IntervalStoreManager> loadIntervalStorageManager() async => _loadFile(
+ 'intervall-store',
+ (String jsonStr) {
+ final json = jsonDecode(jsonStr);
+ if (json is Map<String, dynamic>) {
+ return IntervalStoreManager(
+ json['main'] is! String ? IntervalStorage() : IntervalStorage.fromJson(json['main']!),
+ json['export'] is! String ? IntervalStorage() : IntervalStorage.fromJson(json['export']!),
+ json['stats'] is! String ? IntervalStorage() : IntervalStorage.fromJson(json['stats']!),
+ );
+ }
+ return IntervalStoreManager(IntervalStorage(), IntervalStorage(), IntervalStorage());
+ },
+ () => IntervalStoreManager(IntervalStorage(), IntervalStorage(), IntervalStorage()),
+ (e) => jsonEncode({
+ 'main': e.mainPage.toJson(),
+ 'export': e.exportPage.toJson(),
+ 'stats': e.statsPage.toJson(),
+ }),
+ );
+
+ @override
+ Future<PdfExportSettings> loadPdfExportSettings() async => _loadFile(
+ 'pdf-export',
+ PdfExportSettings.fromJson,
+ PdfExportSettings.new,
+ (e) => e.toJson(),
+ );
+
+ @override
+ Future<Settings> loadSettings() async => _loadFile(
+ 'general',
+ Settings.fromJson,
+ Settings.new,
+ (e) => e.toJson(),
+ );
+
+ /// Attempt to backup all stored data to archive.
+ Archive? createArchive() {
+ try {
+ final archive = Archive();
+ _backupFile(archive, 'general');
+ _backupFile(archive, 'export');
+ _backupFile(archive, 'csv-export');
+ _backupFile(archive, 'pdf-export');
+ _backupFile(archive, 'export-columns');
+ _backupFile(archive, 'intervall-store');
+ return archive;
+ } on FileSystemException {
+ return null;
+ }
+ }
+
+ void _backupFile(Archive archive, String fileName) {
+ final data = File(join(_path, fileName)).readAsStringSync();
+ archive.addFile(ArchiveFile.string(fileName, data));
+ }
+}
app/lib/model/storage/db/settings_loader.dart
@@ -0,0 +1,57 @@
+import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
+import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+
+/// A backend agnostic loader for settings data.
+abstract class SettingsLoader {
+ /// Loads the profiles [Settings] object from disk.
+ ///
+ /// If any errors occur or the object is not present, a default one will be
+ /// created. Changes in the object will save to the automatically.
+ ///
+ /// Changes to the disk data will not propagate to the object.
+ Future<Settings> loadSettings();
+
+ /// Loads the profiles [ExportSettings] object from disk.
+ ///
+ /// If any errors occur or the object is not present, a default one will be
+ /// created. Changes in the object will save to the automatically.
+ ///
+ /// Changes to the disk data will not propagate to the object.
+ Future<ExportSettings> loadExportSettings();
+
+ /// Loads the profiles [CsvExportSettings] object from disk.
+ ///
+ /// If any errors occur or the object is not present, a default one will be
+ /// created. Changes in the object will save to the automatically.
+ ///
+ /// Changes to the disk data will not propagate to the object.
+ Future<CsvExportSettings> loadCsvExportSettings();
+
+ /// Loads the profiles [PdfExportSettings] object from disk.
+ ///
+ /// If any errors occur or the object is not present, a default one will be
+ /// created. Changes in the object will save to the automatically.
+ ///
+ /// Changes to the disk data will not propagate to the object.
+ Future<PdfExportSettings> loadPdfExportSettings();
+
+ /// Loads a [IntervalStoreManager] object from disk.
+ ///
+ /// If any errors occur or the object is not present, a default one will be
+ /// created. Changes in the object will save to the automatically.
+ ///
+ /// Changes to the database will not propagate to the object.
+ Future<IntervalStoreManager> loadIntervalStorageManager();
+
+ /// Loads the profiles [ExportColumnsManager] object from disk.
+ ///
+ /// If any errors occur or the object is not present, a default one will be
+ /// created. Changes in the object will save to the automatically.
+ ///
+ /// Changes to the disk data will not propagate to the object.
+ Future<ExportColumnsManager> loadExportColumnsManager();
+}
app/lib/model/storage/export_columns_store.dart
@@ -42,6 +42,11 @@ class ExportColumnsManager extends ChangeNotifier {
_userColumns.clear();
notifyListeners();
}
+ void copyFrom(ExportColumnsManager other) {
+ _userColumns.clear();
+ _userColumns.addAll(other._userColumns);
+ notifyListeners();
+ }
/// Namespaces that may not lead a user columns internal identifier.
static const List<String> reservedNamespaces = ['buildIn', 'myHeart'];
app/lib/model/storage/export_csv_settings_store.dart
@@ -61,16 +61,18 @@ class CsvExportSettings extends ChangeNotifier implements CustomFieldsSettings {
/// Serializes the object to json string.
String toJson() => jsonEncode(toMap());
- /// Reset all fields to their default values.
- void reset() {
- final d = CsvExportSettings();
- _fieldDelimiter = d.fieldDelimiter;
- _textDelimiter = d._textDelimiter;
- _exportHeadline = d._exportHeadline;
- _exportFieldsConfiguration = d._exportFieldsConfiguration;
+ /// Copy all values from another instance.
+ void copyFrom(CsvExportSettings other) {
+ _fieldDelimiter = other._fieldDelimiter;
+ _textDelimiter = other._textDelimiter;
+ _exportHeadline = other._exportHeadline;
+ _exportFieldsConfiguration = other._exportFieldsConfiguration;
notifyListeners();
}
+ /// Reset all fields to their default values.
+ void reset() => copyFrom(CsvExportSettings());
+
String _fieldDelimiter = ',';
String get fieldDelimiter => _fieldDelimiter;
set fieldDelimiter(String value) {
app/lib/model/storage/export_pdf_settings_store.dart
@@ -64,16 +64,18 @@ class PdfExportSettings extends ChangeNotifier implements CustomFieldsSettings {
String toJson() => jsonEncode(toMap());
/// Reset all fields to their default values.
- void reset() {
- final d = PdfExportSettings();
- _exportTitle = d._exportTitle;
- _exportStatistics = d._exportStatistics;
- _exportData = d._exportData;
- _headerHeight = d._headerHeight;
- _cellHeight = d._cellHeight;
- _headerFontSize = d._headerFontSize;
- _cellFontSize = d._cellFontSize;
- _exportFieldsConfiguration = d._exportFieldsConfiguration;
+ void reset() => copyFrom(PdfExportSettings());
+
+ // Copy all values from another instance.
+ void copyFrom(PdfExportSettings other) {
+ _exportTitle = other._exportTitle;
+ _exportStatistics = other._exportStatistics;
+ _exportData = other._exportData;
+ _headerHeight = other._headerHeight;
+ _cellHeight = other._cellHeight;
+ _headerFontSize = other._headerFontSize;
+ _cellFontSize = other._cellFontSize;
+ _exportFieldsConfiguration = other._exportFieldsConfiguration;
notifyListeners();
}
app/lib/model/storage/export_settings_store.dart
@@ -35,17 +35,20 @@ class ExportSettings extends ChangeNotifier {
'exportAfterEveryEntry': exportAfterEveryEntry,
};
+ /// Serialize the object to a restoreable string.
String toJson() => jsonEncode(toMap());
- /// Reset all fields to their default values.
- void reset() {
- final d = ExportSettings();
- _exportFormat = d._exportFormat;
- _defaultExportDir = d._defaultExportDir;
- _exportAfterEveryEntry = d._exportAfterEveryEntry;
+ /// Copy all values from another instance.
+ void copyFrom(ExportSettings other) {
+ _exportFormat = other._exportFormat;
+ _defaultExportDir = other._defaultExportDir;
+ _exportAfterEveryEntry = other._exportAfterEveryEntry;
notifyListeners();
}
+ /// Reset all fields to their default values.
+ void reset() => copyFrom(ExportSettings());
+
ExportFormat _exportFormat = ExportFormat.csv;
ExportFormat get exportFormat => _exportFormat;
set exportFormat(ExportFormat value) {
app/lib/model/storage/interval_store.dart
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:blood_pressure_app/model/storage/convert_util.dart';
-import 'package:blood_pressure_app/model/storage/db/config_dao.dart';
+import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
@@ -195,21 +195,13 @@ enum TimeStep {
class IntervalStoreManager extends ChangeNotifier {
/// Constructor for creating [IntervalStoreManager] from items.
///
- /// Consider using [IntervalStoreManager.load] for loading [IntervalStorage] objects from the database, as it
- /// automatically uses the correct storage ids.
+ /// You should use [SettingsLoader.loadExportColumnsManager] for most cases.
IntervalStoreManager(this.mainPage, this.exportPage, this.statsPage) {
mainPage.addListener(notifyListeners);
exportPage.addListener(notifyListeners);
statsPage.addListener(notifyListeners);
}
- static Future<IntervalStoreManager> load(ConfigDao configDao, int profileID) async =>
- IntervalStoreManager(
- await configDao.loadIntervalStorage(profileID, 0),
- await configDao.loadIntervalStorage(profileID, 1),
- await configDao.loadIntervalStorage(profileID, 2),
- );
-
IntervalStorage get(IntervalStoreManagerLocation type) => switch (type) {
IntervalStoreManagerLocation.mainPage => mainPage,
IntervalStoreManagerLocation.exportPage => exportPage,
@@ -224,6 +216,14 @@ class IntervalStoreManager extends ChangeNotifier {
notifyListeners();
}
+ // Copy all values from another instance.
+ void copyFrom(IntervalStoreManager other) {
+ mainPage = other.mainPage;
+ exportPage = other.exportPage;
+ statsPage = other.statsPage;
+ notifyListeners();
+ }
+
/// Intervall for the page with graph and list.
IntervalStorage mainPage;
app/lib/model/storage/settings_store.dart
@@ -162,39 +162,42 @@ class Settings extends ChangeNotifier {
/// Serialize the object to a restoreable string.
String toJson() => jsonEncode(toMap());
- /// Reset all fields to their default values.
- void reset() {
- final d = Settings();
- _language = d._language;
- _accentColor = d._accentColor;
- _sysColor = d._sysColor;
- _diaColor = d._diaColor;
- _pulColor = d._pulColor;
- _horizontalGraphLines = d._horizontalGraphLines;
- _dateFormatString = d._dateFormatString;
- _graphLineThickness = d._graphLineThickness;
- _needlePinBarWidth = d._needlePinBarWidth;
- _animationSpeed = d._animationSpeed;
- _sysWarn = d._sysWarn;
- _diaWarn = d._diaWarn;
- _lastVersion = d._lastVersion;
- _allowManualTimeInput = d._allowManualTimeInput;
- _confirmDeletion = d._confirmDeletion;
- _themeMode = d._themeMode;
- _validateInputs = d._validateInputs;
- _allowMissingValues = d._allowMissingValues;
- _drawRegressionLines = d._drawRegressionLines;
- _startWithAddMeasurementPage = d._startWithAddMeasurementPage;
- _useLegacyList = d._useLegacyList;
- _bottomAppBars = d._bottomAppBars;
+ /// Copy all values from another instance.
+ void copyFrom(Settings other) {
+ _language = other._language;
+ _accentColor = other._accentColor;
+ _sysColor = other._sysColor;
+ _diaColor = other._diaColor;
+ _pulColor = other._pulColor;
+ _horizontalGraphLines = other._horizontalGraphLines;
+ _dateFormatString = other._dateFormatString;
+ _graphLineThickness = other._graphLineThickness;
+ _needlePinBarWidth = other._needlePinBarWidth;
+ _animationSpeed = other._animationSpeed;
+ _sysWarn = other._sysWarn;
+ _diaWarn = other._diaWarn;
+ _lastVersion = other._lastVersion;
+ _allowManualTimeInput = other._allowManualTimeInput;
+ _confirmDeletion = other._confirmDeletion;
+ _themeMode = other._themeMode;
+ _validateInputs = other._validateInputs;
+ _allowMissingValues = other._allowMissingValues;
+ _drawRegressionLines = other._drawRegressionLines;
+ _startWithAddMeasurementPage = other._startWithAddMeasurementPage;
+ _useLegacyList = other._useLegacyList;
+ _bottomAppBars = other._bottomAppBars;
+ _preferredPressureUnit = other._preferredPressureUnit;
+ _knownBleDev = other._knownBleDev;
+ _bleInput = other._bleInput;
_medications.clear();
- _highestMedIndex = d._highestMedIndex;
- _preferredPressureUnit = d._preferredPressureUnit;
- _knownBleDev = d._knownBleDev;
- _bleInput = d._bleInput;
+ _medications.addAll(other._medications);
+ _highestMedIndex = other._highestMedIndex;
notifyListeners();
}
+ /// Reset all fields to their default values.
+ void reset() => copyFrom(Settings());
+
Locale? _language;
/// Language to use the app in.
///
app/lib/model/storage/update_legacy_settings.dart
@@ -14,11 +14,14 @@ import 'package:blood_pressure_app/model/storage/interval_store.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
+import 'package:health_data_store/health_data_store.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite/sqflite.dart';
+import 'db/config_dao.dart';
+
/// Function for upgrading shared preferences from pre 1.5.4 (Oct 23) versions.
-Future<void> updateLegacySettings(Settings settings, ExportSettings exportSettings, CsvExportSettings csvExportSettings,
+Future<void> migrateSharedPreferences(Settings settings, ExportSettings exportSettings, CsvExportSettings csvExportSettings,
PdfExportSettings pdfExportSettings, IntervalStoreManager intervallStoreManager,) async {
final SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
@@ -199,7 +202,7 @@ Future<void> updateLegacySettings(Settings settings, ExportSettings exportSettin
/// Function for upgrading pre 1.5.8 columns and settings to new structures.
///
/// - Adds columns from old db table to [manager].
-Future<void> updateLegacyExport(ConfigDB database, ExportColumnsManager manager) async {
+Future<void> _updateLegacyExport(ConfigDB database, ExportColumnsManager manager) async {
if (await _tableExists(database.database, ConfigDB.exportStringsTable)) {
final existingDbEntries = await database.database.query(
ConfigDB.exportStringsTable,
@@ -225,3 +228,35 @@ Future<void> updateLegacyExport(ConfigDB database, ExportColumnsManager manager)
Future<bool> _tableExists(Database database, String tableName) async => (await database.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName';",)).isNotEmpty;
+
+/// Migrate to file based settings format from db in pre 1.7.4 (Jul 24).
+Future<void> migrateDatabaseSettings(
+ Settings settings,
+ ExportSettings exportSettings,
+ CsvExportSettings csvExportSettings,
+ PdfExportSettings pdfExportSettings,
+ IntervalStoreManager intervallStoreManager,
+ ExportColumnsManager manager,
+ MedicineRepository medRepo,
+) async {
+ final configDB = await ConfigDB.open();
+ if(configDB == null) return; // not upgradable
+
+ await _updateLegacyExport(configDB, manager); // TODO: test these older migrations
+ final configDao = ConfigDao(configDB);
+
+ final oldSettings = await configDao.loadSettings();
+ settings.copyFrom(oldSettings);
+ final oldMeds = settings.medications.map((e) => Medicine(
+ designation: e.designation,
+ color: e.color.value,
+ dosis: e.defaultDosis == null ? null : Weight.mg(e.defaultDosis!),
+ ));
+ await Future.forEach(oldMeds, medRepo.add);
+
+ exportSettings.copyFrom(await configDao.loadExportSettings());
+ csvExportSettings.copyFrom(await configDao.loadCsvExportSettings());
+ pdfExportSettings.copyFrom(await configDao.loadPdfExportSettings());
+ intervallStoreManager.copyFrom(await configDao.loadIntervalStorageManager());
+ manager.copyFrom(await configDao.loadExportColumnsManager());
+}
app/lib/screens/settings_screen.dart
@@ -1,6 +1,7 @@
import 'dart:io';
+import 'dart:typed_data';
-import 'package:blood_pressure_app/components/custom_banner.dart';
+import 'package:archive/archive_io.dart';
import 'package:blood_pressure_app/components/input_dialoge.dart';
import 'package:blood_pressure_app/data_util/consistent_future_builder.dart';
import 'package:blood_pressure_app/features/settings/delete_data_screen.dart';
@@ -15,9 +16,14 @@ import 'package:blood_pressure_app/features/settings/tiles/slider_list_tile.dart
import 'package:blood_pressure_app/features/settings/tiles/titled_column.dart';
import 'package:blood_pressure_app/features/settings/version_screen.dart';
import 'package:blood_pressure_app/features/settings/warn_about_screen.dart';
+import 'package:blood_pressure_app/logging.dart';
import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
import 'package:blood_pressure_app/model/blood_pressure/warn_values.dart';
import 'package:blood_pressure_app/model/iso_lang_names.dart';
+import 'package:blood_pressure_app/model/storage/db/config_db.dart';
+import 'package:blood_pressure_app/model/storage/db/file_settings_loader.dart';
+import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
+import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
import 'package:blood_pressure_app/model/storage/storage.dart';
import 'package:blood_pressure_app/platform_integration/platform_client.dart';
import 'package:file_picker/file_picker.dart';
@@ -26,7 +32,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart';
-import 'package:sqflite/sqflite.dart';
import 'package:url_launcher/url_launcher.dart';
/// Primary settings page to manage basic settings and link to subsettings.
@@ -306,11 +311,20 @@ class SettingsPage extends StatelessWidget {
title: Text(localizations.exportSettings),
leading: const Icon(Icons.tune),
onTap: () async {
- String dbPath = await getDatabasesPath();
- assert(dbPath != inMemoryDatabasePath);
- dbPath = join(dbPath, 'config.db');
- assert(Platform.isAndroid);
- await PlatformClient.shareFile(dbPath, 'application/vnd.sqlite3');
+ final messenger = ScaffoldMessenger.of(context);
+ final loader = await FileSettingsLoader.load();
+ final archive = loader.createArchive();
+ if (archive == null) {
+ messenger.showSnackBar(SnackBar(content: Text(localizations.errCantCreateArchive)));
+ return;
+ }
+ final compressedArchive = ZipEncoder().encode(archive);
+ if (compressedArchive == null) {
+ messenger.showSnackBar(SnackBar(content: Text(localizations.errCantCreateArchive)));
+ return;
+ }
+ final archiveData = Uint8List.fromList(compressedArchive);
+ await PlatformClient.shareData(archiveData, 'application/zip', 'bloodPressureSettings.zip');
},
),
ListTile(
@@ -330,11 +344,33 @@ class SettingsPage extends StatelessWidget {
return;
}
- String dbPath = await getDatabasesPath();
- dbPath = join(dbPath, 'config.db');
- File(path).copySync(dbPath);
- messenger.showMaterialBanner(CustomBanner(content: Text(localizations.pleaseRestart)));
- // TODO: read settings and replace them on running app.
+ late SettingsLoader loader;
+ if (path.endsWith('db')) {
+ final configDB = await ConfigDB.open(dbPath: path, isFullPath: true);
+ if(configDB == null) return; // too old (doesn't contain settings yet)
+ loader = ConfigDao(configDB);
+ } else if (path.endsWith('zip')) {
+ try {
+ final decoded = ZipDecoder().decodeBuffer(InputFileStream(result.files.single.path!));
+ final dir = join(Directory.systemTemp.path, 'settingsBackup');
+ await extractArchiveToDisk(decoded, dir);
+ loader = await FileSettingsLoader.load(dir);
+ } on FormatException catch (e, stack) {
+ messenger.showSnackBar(SnackBar(content: Text(localizations.invalidZip)));
+ Log.err('invalid zip', [e, stack]);
+ return;
+ }
+ } else {
+ messenger.showSnackBar(SnackBar(content: Text(localizations.errNotImportable)));
+ return;
+ }
+ settings.copyFrom(await loader.loadSettings());
+ context.read<ExportSettings>().copyFrom(await loader.loadExportSettings());
+ context.read<CsvExportSettings>().copyFrom(await loader.loadCsvExportSettings());
+ context.read<PdfExportSettings>().copyFrom(await loader.loadPdfExportSettings());
+ context.read<IntervalStoreManager>().copyFrom(await loader.loadIntervalStorageManager());
+ context.read<ExportColumnsManager>().copyFrom(await loader.loadExportColumnsManager());
+ messenger.showSnackBar(SnackBar(content: Text(localizations.success(localizations.importSettings))));
},
),
ListTile(
app/lib/app.dart
@@ -3,7 +3,8 @@ import 'dart:io';
import 'package:blood_pressure_app/data_util/consistent_future_builder.dart';
import 'package:blood_pressure_app/model/blood_pressure/update_legacy_entries.dart';
import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
-import 'package:blood_pressure_app/model/storage/db/config_db.dart';
+import 'package:blood_pressure_app/model/storage/db/file_settings_loader.dart';
+import 'package:blood_pressure_app/model/storage/db/settings_loader.dart';
import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
import 'package:blood_pressure_app/model/storage/storage.dart';
import 'package:blood_pressure_app/screens/error_reporting_screen.dart';
@@ -33,8 +34,6 @@ class App extends StatefulWidget {
}
class _AppState extends State<App> {
- /// Database object for app settings.
- ConfigDB? _configDB;
Database? _entryDB;
/// The result of the first [_loadApp] call.
@@ -50,8 +49,6 @@ class _AppState extends State<App> {
@override
void dispose() {
- _configDB?.database.close();
- _configDB = null;
_entryDB?.close();
_entryDB = null;
_settings?.dispose();
@@ -87,17 +84,15 @@ class _AppState extends State<App> {
}
try {
- _configDB = await ConfigDB.open();
- final configDao = ConfigDao(_configDB!);
-
- _settings ??= await configDao.loadSettings(0);
- _exportSettings ??= await configDao.loadExportSettings(0);
- _csvExportSettings ??= await configDao.loadCsvExportSettings(0);
- _pdfExportSettings ??= await configDao.loadPdfExportSettings(0);
- _intervalStorageManager ??= await IntervalStoreManager.load(configDao, 0);
- _exportColumnsManager ??= await configDao.loadExportColumnsManager(0);
+ final SettingsLoader settingsLoader = await FileSettingsLoader.load();
+ _settings ??= await settingsLoader.loadSettings();
+ _exportSettings ??= await settingsLoader.loadExportSettings();
+ _csvExportSettings ??= await settingsLoader.loadCsvExportSettings();
+ _pdfExportSettings ??= await settingsLoader.loadPdfExportSettings();
+ _intervalStorageManager ??= await settingsLoader.loadIntervalStorageManager();
+ _exportColumnsManager ??= await settingsLoader.loadExportColumnsManager();
} catch (e, stack) {
- await ErrorReporting.reportCriticalError('Error loading config db', '$e\n$stack',);
+ await ErrorReporting.reportCriticalError('Error loading settings from files', '$e\n$stack',);
}
late BloodPressureRepository bpRepo;
@@ -129,8 +124,7 @@ class _AppState extends State<App> {
// update logic
if (_settings!.lastVersion == 0) {
- await updateLegacySettings(_settings!, _exportSettings!, _csvExportSettings!, _pdfExportSettings!, _intervalStorageManager!);
- await updateLegacyExport(_configDB!, _exportColumnsManager!);
+ await migrateSharedPreferences(_settings!, _exportSettings!, _csvExportSettings!, _pdfExportSettings!, _intervalStorageManager!);
_settings!.lastVersion = 30;
if (_exportSettings!.exportAfterEveryEntry) {
@@ -157,6 +151,27 @@ class _AppState extends State<App> {
await ErrorReporting.reportCriticalError('Error performing upgrades:', '$e\n$stack',);
}
+ final dbPath = await getDatabasesPath();
+ if (File(join(dbPath, 'config.db')).existsSync()) {
+ try {
+ await migrateDatabaseSettings(
+ _settings!,
+ _exportSettings!,
+ _csvExportSettings!,
+ _pdfExportSettings!,
+ _intervalStorageManager!,
+ _exportColumnsManager!,
+ medRepo,
+ );
+ File(join(dbPath, 'config.db')).copySync(join(dbPath, 'v39_config.db.backup'));
+ File(join(dbPath, 'config.db')).deleteSync();
+ File(join(dbPath, 'config.db-journal')).copySync(join(dbPath, 'v39_config.db-journal.backup'));
+ File(join(dbPath, 'config.db-journal')).deleteSync();
+ } catch (e, stack) {
+ await ErrorReporting.reportCriticalError('Error upgrading to file based settings:', '$e\n$stack',);
+ }
+ }
+
_loadedChild = MultiRepositoryProvider(
providers: [
RepositoryProvider.value(value: bpRepo),
@@ -183,7 +198,7 @@ class _AppState extends State<App> {
@override
Widget build(BuildContext context) {
if (!(kDebugMode && (const bool.fromEnvironment('testing_mode')))
- && _loadedChild != null && _configDB != null && _entryDB != null) {
+ && _loadedChild != null && _settings != null && _entryDB != null) {
return _loadedChild!;
}
return ConsistentFutureBuilder(
app/test/model/storage/db/file_settings_loader_test.dart
@@ -0,0 +1,106 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:blood_pressure_app/model/export_import/column.dart';
+import 'package:blood_pressure_app/model/storage/db/file_settings_loader.dart';
+import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ test('constructs new objects', () async {
+ await _loader((loader) async {
+ await expectLater(loader.loadSettings, returnsNormally);
+ await expectLater(loader.loadExportSettings, returnsNormally);
+ await expectLater(loader.loadCsvExportSettings, returnsNormally);
+ await expectLater(loader.loadPdfExportSettings, returnsNormally);
+ await expectLater(loader.loadIntervalStorageManager, returnsNormally);
+ await expectLater(loader.loadExportColumnsManager, returnsNormally);
+ });
+
+ });
+
+ test('persists changes', () async {
+ await _loader((loader1) async {
+ final loader2 = await FileSettingsLoader.load('tmp');
+
+ final settings1 = await loader1.loadSettings();
+ settings1.sysColor = Colors.blueGrey;
+ final settings2 = await loader2.loadSettings();
+ expect(settings2.sysColor.value, settings1.sysColor.value);
+
+ final exportSettings1 = await loader1.loadExportSettings();
+ exportSettings1.exportFormat = ExportFormat.db;
+ final exportSettings2 = await loader2.loadExportSettings();
+ expect(exportSettings2.exportFormat, exportSettings1.exportFormat);
+
+ final csvExportSettings1 = await loader1.loadCsvExportSettings();
+ csvExportSettings1.fieldDelimiter = 'asdsf.",';
+ final csvExportSettings2 = await loader2.loadCsvExportSettings();
+ expect(csvExportSettings2.fieldDelimiter, csvExportSettings1.fieldDelimiter);
+
+ final pdfExportSettings1 = await loader1.loadPdfExportSettings();
+ pdfExportSettings1.cellHeight = 3.1415926;
+ final pdfExportSettings2 = await loader2.loadPdfExportSettings();
+ expect(pdfExportSettings2.cellHeight, pdfExportSettings1.cellHeight);
+
+ final intervalStorageManager1 = await loader1.loadIntervalStorageManager();
+ intervalStorageManager1.mainPage.changeStepSize(TimeStep.lifetime);
+ final intervalStorageManager2 = await loader2.loadIntervalStorageManager();
+ expect(intervalStorageManager2.mainPage.stepSize, intervalStorageManager1.mainPage.stepSize);
+
+ final exportColumnsManager1 = await loader1.loadExportColumnsManager();
+ exportColumnsManager1.addOrUpdate(TimeColumn('tsttime', 'tst-YYYY'));
+ final exportColumnsManager2 = await loader2.loadExportColumnsManager();
+ expect(exportColumnsManager2.userColumns.keys, exportColumnsManager1.userColumns.keys);
+ });
+ });
+}
+
+Future<void> _loader(void Function(FileSettingsLoader) body) async {
+ final files = {};
+ return IOOverrides.runZoned(
+ () async => body(await FileSettingsLoader.load('tmp')),
+ createDirectory: (path) => _MockDir(),
+ createFile: (path) {
+ if (files[path] == null) files[path] = _MockFile();
+ return files[path];
+ },
+ );
+}
+
+class _MockDir implements Directory {
+ bool _exists = false;
+
+ @override
+ void createSync({bool recursive = false}) => _exists = true;
+
+ @override
+ Future<bool> exists() async => _exists;
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class _MockFile implements File {
+ String? _content;
+
+ @override
+ String readAsStringSync({Encoding encoding = utf8}) {
+ if (_content == null) throw FileSystemException();
+ return _content!;
+ }
+
+ @override
+ void writeAsStringSync(String contents, {
+ FileMode mode = FileMode.write,
+ Encoding encoding = utf8,
+ bool flush = false,
+ }) {
+ _content = contents;
+ }
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
app/test/model/config_db_test.dart
@@ -1,84 +0,0 @@
-import 'package:blood_pressure_app/model/storage/db/config_dao.dart';
-import 'package:blood_pressure_app/model/storage/db/config_db.dart';
-import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
-import 'package:blood_pressure_app/model/storage/export_csv_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/export_settings_store.dart';
-import 'package:blood_pressure_app/model/storage/interval_store.dart';
-import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:sqflite_common_ffi/sqflite_ffi.dart';
-
-void main() {
- group('ConfigDB', () {
- setUpAll(() {
- sqfliteFfiInit();
- // Avoid warning in logs by avoiding `databaseFactory` setter
- databaseFactoryOrNull = databaseFactoryFfi;
- });
-
- test('should initialize database without error', () async {
- final db = await ConfigDB.open(dbPath: inMemoryDatabasePath, isFullPath: true);
- expect(db.database.isOpen, true);
- });
- test('tables should exist', () async {
- final db = await ConfigDB.open(dbPath: inMemoryDatabasePath, isFullPath: true);
- final existingTables = await db.database.query('sqlite_master');
- final tableNames = existingTables.map((e) => e['name'] as String);
-
- expect(tableNames.contains(ConfigDB.settingsTable), true);
- expect(tableNames.contains(ConfigDB.exportSettingsTable), true);
- expect(tableNames.contains(ConfigDB.exportCsvSettingsTable), true);
- expect(tableNames.contains(ConfigDB.exportPdfSettingsTable), true);
- expect(tableNames.contains(ConfigDB.selectedIntervalStorageTable), true);
- expect(tableNames.contains(ConfigDB.exportColumnsTable), true);
- });
- test('should save and load table entries', () async {
- final db = await ConfigDB.open(dbPath: inMemoryDatabasePath, isFullPath: true);
- await db.database.insert(ConfigDB.settingsTable, {'profile_id': 0, 'settings_json': '{"test": 123}'});
- final queriedData = await db.database.query(ConfigDB.settingsTable);
- expect(queriedData.length, 1);
- expect(queriedData[0]['profile_id'], 0);
- expect(queriedData[0]['settings_json'], '{"test": 123}');
- });
- });
-
- group('ConfigDAO', () {
- setUpAll(() {
- sqfliteFfiInit();
- databaseFactoryOrNull = databaseFactoryFfi;
- });
-
- test('should initialize', () async {
- final rawDB = await ConfigDB.open(dbPath: inMemoryDatabasePath, isFullPath: true);
- ConfigDao(rawDB);
- });
- test('should create classes when no data is present', () async {
- final rawDB = await ConfigDB.open(dbPath: inMemoryDatabasePath, isFullPath: true);
- final dao = ConfigDao(rawDB);
-
- expect((await dao.loadSettings(0)).toJson(), Settings().toJson());
- expect((await dao.loadExportSettings(0)).toJson(), ExportSettings().toJson());
- expect((await dao.loadCsvExportSettings(0)).toJson(), CsvExportSettings().toJson());
- expect((await dao.loadPdfExportSettings(0)).toJson(), PdfExportSettings().toJson());
- expect((await dao.loadIntervalStorage(0,0)).stepSize, IntervalStorage().stepSize);
- expect((await dao.loadExportColumnsManager(0)).userColumns, ExportColumnsManager().userColumns);
- });
- test('should save changes', () async {
- final rawDB = await ConfigDB.open(dbPath: inMemoryDatabasePath, isFullPath: true);
- final dao = ConfigDao(rawDB);
-
- final settings = await dao.loadSettings(0);
- settings.dateFormatString = 'Test string';
- settings.sysColor = Colors.deepOrange;
- settings.animationSpeed = 69;
-
- final initialSettingsJson = settings.toJson();
- settings.dispose();
-
- final newSettings = await dao.loadSettings(0);
- expect(newSettings.toJson(), initialSettingsJson);
- });
- });
-}
app/pubspec.lock
@@ -31,7 +31,7 @@ packages:
source: hosted
version: "5.1.1"
archive:
- dependency: transitive
+ dependency: "direct main"
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
@@ -194,10 +194,10 @@ packages:
dependency: "direct main"
description:
name: collection
- sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
- version: "1.19.0"
+ version: "1.18.0"
convert:
dependency: transitive
description:
@@ -945,10 +945,10 @@ packages:
dependency: transitive
description:
name: string_scanner
- sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
- version: "1.3.0"
+ version: "1.2.0"
sync_http:
dependency: transitive
description:
@@ -977,26 +977,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
+ sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
url: "https://pub.dev"
source: hosted
- version: "1.25.8"
+ version: "1.25.7"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
+ sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
- version: "0.7.3"
+ version: "0.7.2"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
+ sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
url: "https://pub.dev"
source: hosted
- version: "0.6.5"
+ version: "0.6.4"
timing:
dependency: transitive
description:
app/pubspec.yaml
@@ -40,6 +40,7 @@ dependencies:
# desktop only
sqflite_common_ffi: ^2.3.3
+ archive: ^3.6.1
dev_dependencies:
integration_test:
@@ -56,4 +57,3 @@ dev_dependencies:
flutter:
uses-material-design: true
generate: true
-
.gitignore
@@ -1,3 +1,8 @@
-/.idea/
/fastlane/metadata/android/en-US/images/phoneScreenshots/resizeAll.sh
-**/failures/*.png
\ No newline at end of file
+**/failures/*.png
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
\ No newline at end of file
blood_pressure_app.iml
@@ -1,156 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
- <component name="NewModuleRootManager" inherit-compiler-output="true">
- <exclude-output />
- <content url="file://$MODULE_DIR$">
- <excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/file_saver/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/file_saver/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/file_saver/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/file_saver/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/file_saver/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/file_saver/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus_linux/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus_linux/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus_linux/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/file_saver/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/file_saver/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/file_saver/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/file_saver/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/file_saver/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/file_saver/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/share_plus/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/share_plus/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/build" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/build" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/health_data_store/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/health_data_store/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/health_data_store/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/.dart_tool/flutter_gen/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/.dart_tool/flutter_gen/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/.dart_tool/flutter_gen/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/package_info_plus/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/shared_preferences_windows/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/build" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/example/.dart_tool" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/example/.pub" />
- <excludeFolder url="file://$MODULE_DIR$/app/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/example/build" />
- </content>
- <orderEntry type="sourceFolder" forTests="false" />
- <orderEntry type="library" name="Dart SDK" level="project" />
- <orderEntry type="library" name="Flutter Plugins" level="project" />
- <orderEntry type="library" name="Dart Packages" level="project" />
- </component>
-</module>
\ No newline at end of file