fullscreen: added address to overlay
This commit is contained in:
parent
05af913d86
commit
e4da59a624
8 changed files with 160 additions and 40 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_service.dart';
|
import 'package:aves/model/metadata_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -21,7 +23,7 @@ class ImageEntry with ChangeNotifier {
|
||||||
final String bucketDisplayName;
|
final String bucketDisplayName;
|
||||||
final int durationMillis;
|
final int durationMillis;
|
||||||
CatalogMetadata catalogMetadata;
|
CatalogMetadata catalogMetadata;
|
||||||
String addressLine, addressCountry;
|
AddressDetails addressDetails;
|
||||||
|
|
||||||
ImageEntry({
|
ImageEntry({
|
||||||
this.uri,
|
this.uri,
|
||||||
|
@ -125,7 +127,7 @@ class ImageEntry with ChangeNotifier {
|
||||||
|
|
||||||
bool get hasGps => isCataloged && catalogMetadata.latitude != null;
|
bool get hasGps => isCataloged && catalogMetadata.latitude != null;
|
||||||
|
|
||||||
bool get isLocated => addressLine != null;
|
bool get isLocated => addressDetails != null;
|
||||||
|
|
||||||
Tuple2<double, double> get latLng => isCataloged ? Tuple2(catalogMetadata.latitude, catalogMetadata.longitude) : null;
|
Tuple2<double, double> get latLng => isCataloged ? Tuple2(catalogMetadata.latitude, catalogMetadata.longitude) : null;
|
||||||
|
|
||||||
|
@ -150,8 +152,13 @@ class ImageEntry with ChangeNotifier {
|
||||||
final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates);
|
final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates);
|
||||||
if (addresses != null && addresses.length > 0) {
|
if (addresses != null && addresses.length > 0) {
|
||||||
final address = addresses.first;
|
final address = addresses.first;
|
||||||
addressLine = address.addressLine;
|
addressDetails = AddressDetails(
|
||||||
addressCountry = address.countryName;
|
contentId: contentId,
|
||||||
|
addressLine: address.addressLine,
|
||||||
|
countryName: address.countryName,
|
||||||
|
adminArea: address.adminArea,
|
||||||
|
locality: address.locality,
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -160,10 +167,20 @@ class ImageEntry with ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get shortAddress {
|
||||||
|
if (!isLocated) return '';
|
||||||
|
|
||||||
|
// admin area examples: Seoul, Geneva, null
|
||||||
|
// locality examples: Mapo-gu, Geneva, Annecy
|
||||||
|
return LinkedHashSet.of(
|
||||||
|
[addressDetails.countryName, addressDetails.adminArea, addressDetails.locality],
|
||||||
|
).where((part) => part != null && part.isNotEmpty).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
bool search(String query) {
|
bool search(String query) {
|
||||||
if (title.toLowerCase().contains(query)) return true;
|
if (title.toLowerCase().contains(query)) return true;
|
||||||
if (catalogMetadata?.xmpSubjects?.toLowerCase()?.contains(query) ?? false) return true;
|
if (catalogMetadata?.xmpSubjects?.toLowerCase()?.contains(query) ?? false) return true;
|
||||||
if (isLocated && addressLine.toLowerCase().contains(query)) return true;
|
if (isLocated && addressDetails.addressLine.toLowerCase().contains(query)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,3 +70,39 @@ class OverlayMetadata {
|
||||||
return 'OverlayMetadata{aperture=$aperture, exposureTime=$exposureTime, focalLength=$focalLength, iso=$iso}';
|
return 'OverlayMetadata{aperture=$aperture, exposureTime=$exposureTime, focalLength=$focalLength, iso=$iso}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AddressDetails {
|
||||||
|
final int contentId;
|
||||||
|
final String addressLine, countryName, adminArea, locality;
|
||||||
|
|
||||||
|
AddressDetails({
|
||||||
|
this.contentId,
|
||||||
|
this.addressLine,
|
||||||
|
this.countryName,
|
||||||
|
this.adminArea,
|
||||||
|
this.locality,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AddressDetails.fromMap(Map map) {
|
||||||
|
return AddressDetails(
|
||||||
|
contentId: map['contentId'],
|
||||||
|
addressLine: map['addressLine'] ?? '',
|
||||||
|
countryName: map['countryName'] ?? '',
|
||||||
|
adminArea: map['adminArea'] ?? '',
|
||||||
|
locality: map['locality'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'contentId': contentId,
|
||||||
|
'addressLine': addressLine,
|
||||||
|
'countryName': countryName,
|
||||||
|
'adminArea': adminArea,
|
||||||
|
'locality': locality,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AddressDetails{contentId=$contentId, addressLine=$addressLine, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ class MetadataDb {
|
||||||
|
|
||||||
Future<String> get path async => join(await getDatabasesPath(), 'metadata.db');
|
Future<String> get path async => join(await getDatabasesPath(), 'metadata.db');
|
||||||
|
|
||||||
static final table = 'metadata';
|
static final metadataTable = 'metadata';
|
||||||
|
static final addressTable = 'address';
|
||||||
|
|
||||||
MetadataDb._private();
|
MetadataDb._private();
|
||||||
|
|
||||||
|
@ -18,10 +19,9 @@ class MetadataDb {
|
||||||
debugPrint('$runtimeType init');
|
debugPrint('$runtimeType init');
|
||||||
_database = openDatabase(
|
_database = openDatabase(
|
||||||
await path,
|
await path,
|
||||||
onCreate: (db, version) {
|
onCreate: (db, version) async {
|
||||||
return db.execute(
|
await db.execute('CREATE TABLE $metadataTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, latitude REAL, longitude REAL)');
|
||||||
'CREATE TABLE $table(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, latitude REAL, longitude REAL)',
|
await db.execute('CREATE TABLE $addressTable(contentId INTEGER PRIMARY KEY, addressLine TEXT, countryName TEXT, adminArea TEXT, locality TEXT)');
|
||||||
);
|
|
||||||
},
|
},
|
||||||
version: 1,
|
version: 1,
|
||||||
);
|
);
|
||||||
|
@ -34,28 +34,55 @@ class MetadataDb {
|
||||||
await init();
|
await init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<CatalogMetadata>> getAll() async {
|
Future<List<CatalogMetadata>> getAllMetadata() async {
|
||||||
debugPrint('$runtimeType getAll');
|
debugPrint('$runtimeType getAllMetadata');
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final maps = await db.query(table);
|
final maps = await db.query(metadataTable);
|
||||||
return maps.map((map) => CatalogMetadata.fromMap(map)).toList();
|
return maps.map((map) => CatalogMetadata.fromMap(map)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<CatalogMetadata> get(int contentId) async {
|
Future<CatalogMetadata> getMetadata(int contentId) async {
|
||||||
debugPrint('$runtimeType get contentId=$contentId');
|
debugPrint('$runtimeType getMetadata contentId=$contentId');
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
List<Map> maps = await db.query(table, where: 'contentId = ?', whereArgs: [contentId]);
|
List<Map> maps = await db.query(metadataTable, where: 'contentId = ?', whereArgs: [contentId]);
|
||||||
if (maps.length > 0) {
|
if (maps.length > 0) {
|
||||||
return CatalogMetadata.fromMap(maps.first);
|
return CatalogMetadata.fromMap(maps.first);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(CatalogMetadata metadata) async {
|
insertMetadata(CatalogMetadata metadata) async {
|
||||||
// debugPrint('$runtimeType insert metadata=$metadata');
|
// debugPrint('$runtimeType insertMetadata metadata=$metadata');
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
await db.insert(
|
await db.insert(
|
||||||
table,
|
metadataTable,
|
||||||
|
metadata.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AddressDetails>> getAllAddresses() async {
|
||||||
|
debugPrint('$runtimeType getAllAddresses');
|
||||||
|
final db = await _database;
|
||||||
|
final maps = await db.query(addressTable);
|
||||||
|
return maps.map((map) => AddressDetails.fromMap(map)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AddressDetails> getAddress(int contentId) async {
|
||||||
|
debugPrint('$runtimeType getAddress contentId=$contentId');
|
||||||
|
final db = await _database;
|
||||||
|
List<Map> maps = await db.query(addressTable, where: 'contentId = ?', whereArgs: [contentId]);
|
||||||
|
if (maps.length > 0) {
|
||||||
|
return AddressDetails.fromMap(maps.first);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertAddress(AddressDetails metadata) async {
|
||||||
|
// debugPrint('$runtimeType insertAddress metadata=$metadata');
|
||||||
|
final db = await _database;
|
||||||
|
await db.insert(
|
||||||
|
addressTable,
|
||||||
metadata.toMap(),
|
metadata.toMap(),
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ class MetadataService {
|
||||||
}) as Map;
|
}) as Map;
|
||||||
result['contentId'] = entry.contentId;
|
result['contentId'] = entry.contentId;
|
||||||
metadata = CatalogMetadata.fromMap(result);
|
metadata = CatalogMetadata.fromMap(result);
|
||||||
metadataDb.insert(metadata);
|
metadataDb.insertMetadata(metadata);
|
||||||
return metadata;
|
return metadata;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('getCatalogMetadata failed with exception=${e.message}');
|
debugPrint('getCatalogMetadata failed with exception=${e.message}');
|
||||||
|
|
|
@ -34,7 +34,9 @@ class AllCollectionPage extends StatelessWidget {
|
||||||
return Navigator.push(
|
return Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => DebugPage(),
|
builder: (context) => DebugPage(
|
||||||
|
entries: entries,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +1,69 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class DebugPage extends StatefulWidget {
|
class DebugPage extends StatefulWidget {
|
||||||
|
final List<ImageEntry> entries;
|
||||||
|
|
||||||
|
const DebugPage({this.entries});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => DebugPageState();
|
State<StatefulWidget> createState() => DebugPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class DebugPageState extends State<DebugPage> {
|
class DebugPageState extends State<DebugPage> {
|
||||||
Future<List<CatalogMetadata>> _dbLoader;
|
Future<List<CatalogMetadata>> _dbMetadataLoader;
|
||||||
|
Future<List<AddressDetails>> _dbAddressLoader;
|
||||||
|
|
||||||
|
List<ImageEntry> get entries => widget.entries;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_dbLoader = metadataDb.getAll();
|
_dbMetadataLoader = metadataDb.getAllMetadata();
|
||||||
|
_dbAddressLoader = metadataDb.getAllAddresses();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final Map<String, List<ImageEntry>> byMimeTypes = groupBy(entries, (entry) => entry.mimeType);
|
||||||
|
final cataloged = entries.where((entry) => entry.isCataloged);
|
||||||
|
final withGps = cataloged.where((entry) => entry.hasGps);
|
||||||
|
final located = withGps.where((entry) => entry.isLocated);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Info'),
|
title: Text('Info'),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text('Entries: ${entries.length}'),
|
||||||
|
...byMimeTypes.keys.map((mimeType) => Text('- $mimeType: ${byMimeTypes[mimeType].length}')),
|
||||||
|
Text('Cataloged: ${cataloged.length}'),
|
||||||
|
Text('With GPS: ${withGps.length}'),
|
||||||
|
Text('With address: ${located.length}'),
|
||||||
|
Divider(),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
onPressed: () => metadataDb.reset(),
|
onPressed: () => metadataDb.reset(),
|
||||||
child: Text('Reset DB'),
|
child: Text('Reset DB'),
|
||||||
),
|
),
|
||||||
Expanded(
|
FutureBuilder(
|
||||||
child: FutureBuilder(
|
future: _dbMetadataLoader,
|
||||||
future: _dbLoader,
|
builder: (futureContext, AsyncSnapshot<List<CatalogMetadata>> snapshot) {
|
||||||
builder: (futureContext, AsyncSnapshot<List<CatalogMetadata>> snapshot) {
|
if (snapshot.hasError) return Text(snapshot.error);
|
||||||
if (snapshot.hasError) return Text(snapshot.error);
|
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
|
||||||
if (snapshot.connectionState != ConnectionState.done)
|
return Text('DB metadata rows: ${snapshot.data.length}');
|
||||||
return Center(
|
},
|
||||||
child: CircularProgressIndicator(),
|
),
|
||||||
);
|
FutureBuilder(
|
||||||
final metadata = snapshot.data;
|
future: _dbAddressLoader,
|
||||||
return ListView.builder(
|
builder: (futureContext, AsyncSnapshot<List<AddressDetails>> snapshot) {
|
||||||
itemBuilder: (context, index) => Text(' $index: ${metadata[index]}'),
|
if (snapshot.hasError) return Text(snapshot.error);
|
||||||
itemCount: metadata.length,
|
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
|
||||||
);
|
return Text('DB address rows: ${snapshot.data.length}');
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -20,7 +20,7 @@ class LocationSection extends AnimatedWidget {
|
||||||
if (entry.isLocated)
|
if (entry.isLocated)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 8),
|
padding: EdgeInsets.only(top: 8),
|
||||||
child: InfoRow('Address', entry.addressLine),
|
child: InfoRow('Address', entry.addressDetails.addressLine),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -118,6 +118,24 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (entry.isLocated) ...[
|
||||||
|
SizedBox(height: 4),
|
||||||
|
SizedBox(
|
||||||
|
width: subRowWidth,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.place, size: 16),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
entry.shortAddress,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: subRowWidth,
|
width: subRowWidth,
|
||||||
|
|
Loading…
Reference in a new issue