diff --git a/lib/main.dart b/lib/main.dart index 54d257c22..a12bffd0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:aves/model/image_decode_service.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/metadata_storage_service.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/common/fake_app_bar.dart'; import 'package:flutter/material.dart'; @@ -39,15 +40,21 @@ class _HomePageState extends State { void initState() { super.initState(); imageCache.maximumSizeBytes = 100 * 1024 * 1024; + setup(); + } + + setup() async { + await metadataDb.init(); + eventChannel.receiveBroadcastStream().cast().listen( (entryMap) => setState(() => entries.add(ImageEntry.fromMap(entryMap))), - onDone: () { - debugPrint('mediastore stream done'); - setState(() => done = true); - }, - onError: (error) => debugPrint('mediastore stream error=$error'), - ); - ImageDecodeService.getImageEntries(); + onDone: () { + debugPrint('mediastore stream done'); + setState(() => done = true); + }, + onError: (error) => debugPrint('mediastore stream error=$error'), + ); + await ImageDecodeService.getImageEntries(); } @override diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 111c1044e..29b19ebd3 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -1,3 +1,5 @@ +import 'package:geocoder/model.dart'; + import 'mime_types.dart'; class ImageEntry { @@ -101,3 +103,35 @@ class ImageEntry { return '${d.inHours}:$twoDigitMinutes:$twoDigitSeconds'; } } + +class CatalogMetadata { + final int contentId, dateMillis; + final String keywords; + final double latitude, longitude; + Address address; + + CatalogMetadata({this.contentId, this.dateMillis, this.keywords, this.latitude, this.longitude}); + + factory CatalogMetadata.fromMap(Map map) { + return CatalogMetadata( + contentId: map['contentId'], + dateMillis: map['dateMillis'], + keywords: map['keywords'], + latitude: map['latitude'], + longitude: map['longitude'], + ); + } + + Map toMap() => { + 'contentId': contentId, + 'dateMillis': dateMillis, + 'keywords': keywords, + 'latitude': latitude, + 'longitude': longitude, + }; + + @override + String toString() { + return 'CatalogMetadata{contentId: $contentId, dateMillis: $dateMillis, latitude: $latitude, longitude: $longitude, keywords=$keywords}'; + } +} diff --git a/lib/model/metadata_service.dart b/lib/model/metadata_service.dart index 624faa928..002d92b61 100644 --- a/lib/model/metadata_service.dart +++ b/lib/model/metadata_service.dart @@ -1,3 +1,5 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/metadata_storage_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -17,21 +19,25 @@ class MetadataService { return Map(); } - // return map with: - // 'dateMillis': date taken in milliseconds since Epoch (long) - // 'latitude': latitude (double) - // 'longitude': longitude (double) - // 'keywords': space separated XMP subjects (string) - static Future getCatalogMetadata(String path) async { + static Future getCatalogMetadata(int contentId, String path) async { + CatalogMetadata metadata; try { + // return map with: + // 'dateMillis': date taken in milliseconds since Epoch (long) + // 'latitude': latitude (double) + // 'longitude': longitude (double) + // 'keywords': space separated XMP subjects (string) final result = await platform.invokeMethod('getCatalogMetadata', { 'path': path, - }); - return result as Map; + }) as Map; + result['contentId'] = contentId; + metadata = CatalogMetadata.fromMap(result); + metadataDb.insert(metadata); + return metadata; } on PlatformException catch (e) { debugPrint('getCatalogMetadata failed with exception=${e.message}'); } - return Map(); + return null; } // return map with string descriptions for: 'aperture' 'exposureTime' 'focalLength' 'iso' diff --git a/lib/model/metadata_storage_service.dart b/lib/model/metadata_storage_service.dart new file mode 100644 index 000000000..82efc88e0 --- /dev/null +++ b/lib/model/metadata_storage_service.dart @@ -0,0 +1,63 @@ +import 'package:aves/model/image_entry.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 final table = 'metadata'; + + MetadataDb._private(); + + init() async { + debugPrint('$runtimeType init'); + _database = openDatabase( + await path, + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE $table(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, keywords TEXT, latitude REAL, longitude REAL)', + ); + }, + version: 1, + ); + } + + reset() async { + debugPrint('$runtimeType reset'); + (await _database).close(); + deleteDatabase(await path); + await init(); + } + + Future> getAll() async { + debugPrint('$runtimeType getAll'); + final db = await _database; + final maps = await db.query(table); + return maps.map((map) => CatalogMetadata.fromMap(map)).toList(); + } + + Future get(int contentId) async { + debugPrint('$runtimeType get contentId=$contentId'); + final db = await _database; + List maps = await db.query(table, where: 'contentId = ?', whereArgs: [contentId]); + if (maps.length > 0) { + return CatalogMetadata.fromMap(maps.first); + } + return null; + } + + insert(CatalogMetadata metadata) async { + debugPrint('$runtimeType insert metadata=$metadata'); + final db = await _database; + await db.insert( + table, + metadata.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } +} diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 9ee86cba2..b15abb25f 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -1,8 +1,9 @@ import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/album/thumbnail.dart'; import 'package:aves/utils/date_utils.dart'; +import 'package:aves/widgets/album/thumbnail.dart'; import 'package:aves/widgets/common/draggable_scrollbar.dart'; import 'package:aves/widgets/common/outlined_text.dart'; +import 'package:aves/widgets/debug_page.dart'; import 'package:aves/widgets/fullscreen/image_page.dart'; import "package:collection/collection.dart"; import 'package:flutter/material.dart'; @@ -39,6 +40,9 @@ class ThumbnailCollection extends StatelessWidget { slivers: [ SliverAppBar( title: Text('Aves - All'), + actions: [ + IconButton(icon: Icon(Icons.whatshot), onPressed: () => goToDebug(context)), + ], floating: true, ), ...sectionKeys.map((sectionKey) { @@ -66,6 +70,15 @@ class ThumbnailCollection extends StatelessWidget { ), ); } + + Future goToDebug(BuildContext context) { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DebugPage(), + ), + ); + } } class SectionSliver extends StatelessWidget { diff --git a/lib/widgets/debug_page.dart b/lib/widgets/debug_page.dart new file mode 100644 index 000000000..752ebefc4 --- /dev/null +++ b/lib/widgets/debug_page.dart @@ -0,0 +1,52 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/metadata_storage_service.dart'; +import 'package:flutter/material.dart'; + +class DebugPage extends StatefulWidget { + @override + State createState() => DebugPageState(); +} + +class DebugPageState extends State { + Future> _dbLoader; + + @override + void initState() { + super.initState(); + _dbLoader = metadataDb.getAll(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Info'), + ), + body: Column( + children: [ + RaisedButton( + onPressed: () => metadataDb.reset(), + child: Text('Reset DB'), + ), + Expanded( + child: FutureBuilder( + future: _dbLoader, + builder: (futureContext, AsyncSnapshot> snapshot) { + if (snapshot.hasError) return Text(snapshot.error); + if (snapshot.connectionState != ConnectionState.done) + return Center( + child: CircularProgressIndicator(), + ); + final metadata = snapshot.data; + return ListView.builder( + itemBuilder: (context, index) => Text(' $index: ${metadata[index]}'), + itemCount: metadata.length, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/fullscreen/info_page.dart b/lib/widgets/fullscreen/info_page.dart index ae47c9df0..19ba9afa8 100644 --- a/lib/widgets/fullscreen/info_page.dart +++ b/lib/widgets/fullscreen/info_page.dart @@ -18,7 +18,8 @@ class InfoPage extends StatefulWidget { } class InfoPageState extends State { - Future _catalogLoader, _metadataLoader; + Future _metadataLoader; + Future _catalogLoader; bool _scrollStartFromTop = false; ImageEntry get entry => widget.entry; @@ -36,14 +37,14 @@ class InfoPageState extends State { } initMetadataLoader() { - _catalogLoader = MetadataService.getCatalogMetadata(entry.path).then((metadata) async { - final latitude = metadata['latitude']; - final longitude = metadata['longitude']; + _catalogLoader = MetadataService.getCatalogMetadata(entry.contentId, entry.path).then((metadata) async { + final latitude = metadata.latitude; + final longitude = metadata.longitude; if (latitude != null && longitude != null) { final coordinates = Coordinates(latitude, longitude); final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates); if (addresses != null && addresses.length > 0) { - metadata['address'] = addresses.first; + metadata.address = addresses.first; } } return metadata; @@ -96,15 +97,15 @@ class InfoPageState extends State { InfoRow('Path', entry.path), FutureBuilder( future: _catalogLoader, - builder: (futureContext, AsyncSnapshot snapshot) { + builder: (futureContext, AsyncSnapshot snapshot) { if (snapshot.hasError) return Text(snapshot.error); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); final metadata = snapshot.data; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ..._buildLocationSection(metadata['latitude'], metadata['longitude'], metadata['address']), - ..._buildTagSection(metadata['keywords']), + ..._buildLocationSection(metadata.latitude, metadata.longitude, metadata.address), + ..._buildTagSection(metadata.keywords), ], ); }, diff --git a/pubspec.lock b/pubspec.lock index 5917b14d2..b2557c4b7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -103,7 +103,7 @@ packages: source: hosted version: "0.3.0" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" @@ -149,6 +149,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.5" + sqflite: + dependency: "direct main" + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6+3" stack_trace: dependency: transitive description: @@ -170,6 +177,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0+1" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5068ce354..0537ff0dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,8 +25,10 @@ dependencies: geocoder: google_maps_flutter: intl: + path: photo_view: screen: + sqflite: transparent_image: dev_dependencies: