import 'dart:io'; import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db_upgrade.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; final MetadataDb metadataDb = MetadataDb._private(); class MetadataDb { Future _database; Future get path async => join(await getDatabasesPath(), 'metadata.db'); static const entryTable = 'entry'; static const dateTakenTable = 'dateTaken'; static const metadataTable = 'metadata'; static const addressTable = 'address'; static const favouriteTable = 'favourites'; MetadataDb._private(); Future init() async { debugPrint('$runtimeType init'); _database = openDatabase( await path, onCreate: (db, version) async { await db.execute('CREATE TABLE $entryTable(' 'contentId INTEGER PRIMARY KEY' ', uri TEXT' ', path TEXT' ', sourceMimeType TEXT' ', width INTEGER' ', height INTEGER' ', sourceRotationDegrees INTEGER' ', sizeBytes INTEGER' ', title TEXT' ', dateModifiedSecs INTEGER' ', sourceDateTakenMillis INTEGER' ', durationMillis INTEGER' ')'); await db.execute('CREATE TABLE $dateTakenTable(' 'contentId INTEGER PRIMARY KEY' ', dateMillis INTEGER' ')'); await db.execute('CREATE TABLE $metadataTable(' 'contentId INTEGER PRIMARY KEY' ', mimeType TEXT' ', dateMillis INTEGER' ', flags INTEGER' ', rotationDegrees INTEGER' ', xmpSubjects TEXT' ', xmpTitleDescription TEXT' ', latitude REAL' ', longitude REAL' ')'); await db.execute('CREATE TABLE $addressTable(' 'contentId INTEGER PRIMARY KEY' ', addressLine TEXT' ', countryCode TEXT' ', countryName TEXT' ', adminArea TEXT' ', locality TEXT' ')'); await db.execute('CREATE TABLE $favouriteTable(' 'contentId INTEGER PRIMARY KEY' ', path TEXT' ')'); }, onUpgrade: MetadataDbUpgrader.upgradeDb, version: 3, ); } Future dbFileSize() async { final file = File((await path)); return await file.exists() ? file.length() : 0; } Future reset() async { debugPrint('$runtimeType reset'); await (await _database).close(); await deleteDatabase(await path); await init(); } void removeIds(Set contentIds, {@required bool updateFavourites}) async { if (contentIds == null || contentIds.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; // using array in `whereArgs` and using it with `where contentId IN ?` is a pain, so we prefer `batch` instead final batch = db.batch(); const where = 'contentId = ?'; contentIds.forEach((id) { final whereArgs = [id]; batch.delete(entryTable, where: where, whereArgs: whereArgs); batch.delete(dateTakenTable, where: where, whereArgs: whereArgs); batch.delete(metadataTable, where: where, whereArgs: whereArgs); batch.delete(addressTable, where: where, whereArgs: whereArgs); if (updateFavourites) { batch.delete(favouriteTable, where: where, whereArgs: whereArgs); } }); await batch.commit(noResult: true); debugPrint('$runtimeType removeIds complete in ${stopwatch.elapsed.inMilliseconds}ms for ${contentIds.length} entries'); } // entries Future clearEntries() async { final db = await _database; final count = await db.delete(entryTable, where: '1'); debugPrint('$runtimeType clearEntries deleted $count entries'); } Future> loadEntries() async { final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(entryTable); final entries = maps.map((map) => AvesEntry.fromMap(map)).toSet(); debugPrint('$runtimeType loadEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); return entries; } Future saveEntries(Iterable entries) async { if (entries == null || entries.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; final batch = db.batch(); entries.forEach((entry) => _batchInsertEntry(batch, entry)); await batch.commit(noResult: true); debugPrint('$runtimeType saveEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries'); } Future updateEntryId(int oldId, AvesEntry entry) async { final db = await _database; final batch = db.batch(); batch.delete(entryTable, where: 'contentId = ?', whereArgs: [oldId]); _batchInsertEntry(batch, entry); await batch.commit(noResult: true); } void _batchInsertEntry(Batch batch, AvesEntry entry) { if (entry == null) return; batch.insert( entryTable, entry.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } // date taken Future clearDates() async { final db = await _database; final count = await db.delete(dateTakenTable, where: '1'); debugPrint('$runtimeType clearDates deleted $count entries'); } Future> loadDates() async { // final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(dateTakenTable); final metadataEntries = maps.map((map) => DateMetadata.fromMap(map)).toList(); // debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); return metadataEntries; } // catalog metadata Future clearMetadataEntries() async { final db = await _database; final count = await db.delete(metadataTable, where: '1'); debugPrint('$runtimeType clearMetadataEntries deleted $count entries'); } Future> loadMetadataEntries() async { // final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(metadataTable); final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList(); // debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); return metadataEntries; } Future saveMetadata(Iterable metadataEntries) async { if (metadataEntries == null || metadataEntries.isEmpty) return; final stopwatch = Stopwatch()..start(); try { final db = await _database; final batch = db.batch(); metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata)); await batch.commit(noResult: true); debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); } catch (error, stack) { debugPrint('$runtimeType failed to save metadata with exception=$error\n$stack'); } } Future updateMetadataId(int oldId, CatalogMetadata metadata) async { final db = await _database; final batch = db.batch(); batch.delete(dateTakenTable, where: 'contentId = ?', whereArgs: [oldId]); batch.delete(metadataTable, where: 'contentId = ?', whereArgs: [oldId]); _batchInsertMetadata(batch, metadata); await batch.commit(noResult: true); } void _batchInsertMetadata(Batch batch, CatalogMetadata metadata) { if (metadata == null) return; if (metadata.dateMillis != 0) { batch.insert( dateTakenTable, DateMetadata(contentId: metadata.contentId, dateMillis: metadata.dateMillis).toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } batch.insert( metadataTable, metadata.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } // address Future clearAddresses() async { final db = await _database; final count = await db.delete(addressTable, where: '1'); debugPrint('$runtimeType clearAddresses deleted $count entries'); } Future> loadAddresses() async { // final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(addressTable); final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList(); // debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries'); return addresses; } Future saveAddresses(Iterable addresses) async { if (addresses == null || addresses.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; final batch = db.batch(); addresses.where((address) => address != null).forEach((address) => _batchInsertAddress(batch, address)); await batch.commit(noResult: true); debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries'); } Future updateAddressId(int oldId, AddressDetails address) async { final db = await _database; final batch = db.batch(); batch.delete(addressTable, where: 'contentId = ?', whereArgs: [oldId]); _batchInsertAddress(batch, address); await batch.commit(noResult: true); } void _batchInsertAddress(Batch batch, AddressDetails address) { if (address == null) return; batch.insert( addressTable, address.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } // favourites Future clearFavourites() async { final db = await _database; final count = await db.delete(favouriteTable, where: '1'); debugPrint('$runtimeType clearFavourites deleted $count entries'); } Future> loadFavourites() async { // final stopwatch = Stopwatch()..start(); final db = await _database; final maps = await db.query(favouriteTable); final favouriteRows = maps.map((map) => FavouriteRow.fromMap(map)).toList(); // debugPrint('$runtimeType loadFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries'); return favouriteRows; } Future addFavourites(Iterable favouriteRows) async { if (favouriteRows == null || favouriteRows.isEmpty) return; // final stopwatch = Stopwatch()..start(); final db = await _database; final batch = db.batch(); favouriteRows.where((row) => row != null).forEach((row) => _batchInsertFavourite(batch, row)); await batch.commit(noResult: true); // debugPrint('$runtimeType addFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms for ${favouriteRows.length} entries'); } Future updateFavouriteId(int oldId, FavouriteRow row) async { final db = await _database; final batch = db.batch(); batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [oldId]); _batchInsertFavourite(batch, row); await batch.commit(noResult: true); } void _batchInsertFavourite(Batch batch, FavouriteRow row) { if (row == null) return; batch.insert( favouriteTable, row.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } Future removeFavourites(Iterable favouriteRows) async { if (favouriteRows == null || favouriteRows.isEmpty) return; final ids = favouriteRows.where((row) => row != null).map((row) => row.contentId); if (ids.isEmpty) return; final db = await _database; // using array in `whereArgs` and using it with `where contentId IN ?` is a pain, so we prefer `batch` instead final batch = db.batch(); ids.forEach((id) => batch.delete(favouriteTable, where: 'contentId = ?', whereArgs: [id])); await batch.commit(noResult: true); } }