improved reverse geocoding + misc fixes
This commit is contained in:
parent
dc287e8667
commit
a29cc971b2
19 changed files with 84 additions and 73 deletions
|
@ -149,7 +149,7 @@ object StorageUtils {
|
||||||
return paths.map { ensureTrailingSeparator(it) }.toTypedArray()
|
return paths.map { ensureTrailingSeparator(it) }.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
// return physicalPaths based on phone model
|
// returns physicalPaths based on phone model
|
||||||
@SuppressLint("SdCardPath")
|
@SuppressLint("SdCardPath")
|
||||||
private val physicalPaths = arrayOf(
|
private val physicalPaths = arrayOf(
|
||||||
"/storage/sdcard0",
|
"/storage/sdcard0",
|
||||||
|
|
|
@ -17,12 +17,12 @@ class CountryTopology {
|
||||||
|
|
||||||
Future<Topology> getTopology() => _topology != null ? SynchronousFuture(_topology) : rootBundle.loadString(topoJsonAsset).then(TopoJson().parse);
|
Future<Topology> getTopology() => _topology != null ? SynchronousFuture(_topology) : rootBundle.loadString(topoJsonAsset).then(TopoJson().parse);
|
||||||
|
|
||||||
// return the country containing given coordinates
|
// returns the country containing given coordinates
|
||||||
Future<CountryCode> countryCode(LatLng position) async {
|
Future<CountryCode> countryCode(LatLng position) async {
|
||||||
return _countryOfNumeric(await numericCode(position));
|
return _countryOfNumeric(await numericCode(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the ISO 3166-1 numeric code of the country containing given coordinates
|
// returns the ISO 3166-1 numeric code of the country containing given coordinates
|
||||||
Future<int> numericCode(LatLng position) async {
|
Future<int> numericCode(LatLng position) async {
|
||||||
final topology = await getTopology();
|
final topology = await getTopology();
|
||||||
if (topology == null) return null;
|
if (topology == null) return null;
|
||||||
|
@ -31,7 +31,7 @@ class CountryTopology {
|
||||||
return _getNumeric(topology, countries, position);
|
return _getNumeric(topology, countries, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return a map of the given positions by country
|
// returns a map of the given positions by country
|
||||||
Future<Map<CountryCode, Set<LatLng>>> countryCodeMap(Set<LatLng> positions) async {
|
Future<Map<CountryCode, Set<LatLng>>> countryCodeMap(Set<LatLng> positions) async {
|
||||||
final numericMap = await numericCodeMap(positions);
|
final numericMap = await numericCodeMap(positions);
|
||||||
numericMap.remove(null);
|
numericMap.remove(null);
|
||||||
|
@ -43,7 +43,7 @@ class CountryTopology {
|
||||||
return codeMap;
|
return codeMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return a map of the given positions by the ISO 3166-1 numeric code of the country containing them
|
// returns a map of the given positions by the ISO 3166-1 numeric code of the country containing them
|
||||||
Future<Map<int, Set<LatLng>>> numericCodeMap(Set<LatLng> positions) async {
|
Future<Map<int, Set<LatLng>>> numericCodeMap(Set<LatLng> positions) async {
|
||||||
final topology = await getTopology();
|
final topology = await getTopology();
|
||||||
if (topology == null) return null;
|
if (topology == null) return null;
|
||||||
|
@ -68,10 +68,18 @@ class CountryTopology {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int _getNumeric(Topology topology, List<Geometry> countries, LatLng position) {
|
static int _getNumeric(Topology topology, List<Geometry> mruCountries, LatLng position) {
|
||||||
final point = [position.longitude, position.latitude];
|
final point = [position.longitude, position.latitude];
|
||||||
final hit = countries.firstWhere((country) => country.containsPoint(topology, point), orElse: () => null);
|
final hit = mruCountries.firstWhere((country) => country.containsPoint(topology, point), orElse: () => null);
|
||||||
final idString = (hit?.id as String);
|
if (hit == null) return null;
|
||||||
|
|
||||||
|
// promote hit countries, assuming given positions are likely to come from the same countries
|
||||||
|
if (mruCountries.first != hit) {
|
||||||
|
mruCountries.remove(hit);
|
||||||
|
mruCountries.insert(0, hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
final idString = (hit.id as String);
|
||||||
final code = idString == null ? null : int.tryParse(idString);
|
final code = idString == null ? null : int.tryParse(idString);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ String _decimal2sexagesimal(final double degDecimal) {
|
||||||
return '$deg° $min′ ${roundToPrecision(sec, decimals: 2).toStringAsFixed(2)}″';
|
return '$deg° $min′ ${roundToPrecision(sec, decimals: 2).toStringAsFixed(2)}″';
|
||||||
}
|
}
|
||||||
|
|
||||||
// return coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E']
|
// returns coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E']
|
||||||
List<String> toDMS(LatLng latLng) {
|
List<String> toDMS(LatLng latLng) {
|
||||||
if (latLng == null) return [];
|
if (latLng == null) return [];
|
||||||
final lat = latLng.latitude;
|
final lat = latLng.latitude;
|
||||||
|
|
|
@ -50,7 +50,7 @@ class AvesEntry {
|
||||||
final Future<List<Address>> Function(Coordinates coordinates) _findAddresses = Geocoder.local.findAddressesFromCoordinates;
|
final Future<List<Address>> Function(Coordinates coordinates) _findAddresses = Geocoder.local.findAddressesFromCoordinates;
|
||||||
|
|
||||||
// TODO TLAD make it dynamic if it depends on OS/lib versions
|
// TODO TLAD make it dynamic if it depends on OS/lib versions
|
||||||
static const List<String> undecodable = [MimeTypes.crw, MimeTypes.psd];
|
static const List<String> undecodable = [MimeTypes.crw, MimeTypes.djvu, MimeTypes.psd];
|
||||||
|
|
||||||
AvesEntry({
|
AvesEntry({
|
||||||
this.uri,
|
this.uri,
|
||||||
|
@ -289,6 +289,8 @@ class AvesEntry {
|
||||||
static const ratioSeparator = '\u2236';
|
static const ratioSeparator = '\u2236';
|
||||||
static const resolutionSeparator = ' \u00D7 ';
|
static const resolutionSeparator = ' \u00D7 ';
|
||||||
|
|
||||||
|
bool get isSized => (width ?? 0) > 0 && (height ?? 0) > 0;
|
||||||
|
|
||||||
String get resolutionText {
|
String get resolutionText {
|
||||||
final ws = width ?? '?';
|
final ws = width ?? '?';
|
||||||
final hs = height ?? '?';
|
final hs = height ?? '?';
|
||||||
|
@ -369,11 +371,15 @@ class AvesEntry {
|
||||||
return _durationText;
|
return _durationText;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasGps => _catalogMetadata?.latitude != null;
|
// returns whether this entry has GPS coordinates
|
||||||
|
// (0, 0) coordinates are considered invalid, as it is likely a default value
|
||||||
|
bool get hasGps => _catalogMetadata != null && _catalogMetadata.latitude != null && _catalogMetadata.longitude != null && (_catalogMetadata.latitude != 0 || _catalogMetadata.longitude != 0);
|
||||||
|
|
||||||
bool get hasAddress => _addressDetails != null;
|
bool get hasAddress => _addressDetails != null;
|
||||||
|
|
||||||
bool get hasPlace => _addressDetails?.place?.isNotEmpty == true;
|
// has a place, or at least the full country name
|
||||||
|
// derived from Google reverse geocoding addresses
|
||||||
|
bool get hasFineAddress => _addressDetails != null && (_addressDetails.place?.isNotEmpty == true || (_addressDetails.countryName?.length ?? 0) > 3);
|
||||||
|
|
||||||
LatLng get latLng => hasGps ? LatLng(_catalogMetadata.latitude, _catalogMetadata.longitude) : null;
|
LatLng get latLng => hasGps ? LatLng(_catalogMetadata.latitude, _catalogMetadata.longitude) : null;
|
||||||
|
|
||||||
|
@ -449,22 +455,23 @@ class AvesEntry {
|
||||||
addressChangeNotifier.notifyListeners();
|
addressChangeNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> locate() async {
|
Future<void> locate({@required bool background}) async {
|
||||||
|
if (!hasGps) return;
|
||||||
await _locateCountry();
|
await _locateCountry();
|
||||||
if (await availability.canLocatePlaces) {
|
if (await availability.canLocatePlaces) {
|
||||||
await locatePlace(background: false);
|
await locatePlace(background: background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// quick reverse geocoding to find the country, using an offline asset
|
// quick reverse geocoding to find the country, using an offline asset
|
||||||
Future<void> _locateCountry() async {
|
Future<void> _locateCountry() async {
|
||||||
if (hasAddress) return;
|
if (!hasGps || hasAddress) return;
|
||||||
final countryCode = await countryTopology.countryCode(latLng);
|
final countryCode = await countryTopology.countryCode(latLng);
|
||||||
setCountry(countryCode);
|
setCountry(countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCountry(CountryCode countryCode) {
|
void setCountry(CountryCode countryCode) {
|
||||||
if (hasPlace || countryCode == null) return;
|
if (hasFineAddress || countryCode == null) return;
|
||||||
addressDetails = AddressDetails(
|
addressDetails = AddressDetails(
|
||||||
contentId: contentId,
|
contentId: contentId,
|
||||||
countryCode: countryCode.alpha2,
|
countryCode: countryCode.alpha2,
|
||||||
|
@ -474,16 +481,10 @@ class AvesEntry {
|
||||||
|
|
||||||
// full reverse geocoding, requiring Play Services and some connectivity
|
// full reverse geocoding, requiring Play Services and some connectivity
|
||||||
Future<void> locatePlace({@required bool background}) async {
|
Future<void> locatePlace({@required bool background}) async {
|
||||||
if (hasPlace) return;
|
if (!hasGps || hasFineAddress) return;
|
||||||
|
final coordinates = latLng;
|
||||||
await catalog(background: background);
|
|
||||||
final latitude = _catalogMetadata?.latitude;
|
|
||||||
final longitude = _catalogMetadata?.longitude;
|
|
||||||
if (latitude == null || longitude == null || (latitude == 0 && longitude == 0)) return;
|
|
||||||
|
|
||||||
final coordinates = Coordinates(latitude, longitude);
|
|
||||||
try {
|
try {
|
||||||
Future<List<Address>> call() => _findAddresses(coordinates);
|
Future<List<Address>> call() => _findAddresses(Coordinates(coordinates.latitude, coordinates.longitude));
|
||||||
final addresses = await (background
|
final addresses = await (background
|
||||||
? servicePolicy.call(
|
? servicePolicy.call(
|
||||||
call,
|
call,
|
||||||
|
@ -511,13 +512,11 @@ class AvesEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> findAddressLine() async {
|
Future<String> findAddressLine() async {
|
||||||
final latitude = _catalogMetadata?.latitude;
|
if (!hasGps) return null;
|
||||||
final longitude = _catalogMetadata?.longitude;
|
|
||||||
if (latitude == null || longitude == null || (latitude == 0 && longitude == 0)) return null;
|
|
||||||
|
|
||||||
final coordinates = Coordinates(latitude, longitude);
|
final coordinates = latLng;
|
||||||
try {
|
try {
|
||||||
final addresses = await _findAddresses(coordinates);
|
final addresses = await _findAddresses(Coordinates(coordinates.latitude, coordinates.longitude));
|
||||||
if (addresses != null && addresses.isNotEmpty) {
|
if (addresses != null && addresses.isNotEmpty) {
|
||||||
final address = addresses.first;
|
final address = addresses.first;
|
||||||
return address.addressLine;
|
return address.addressLine;
|
||||||
|
|
|
@ -23,6 +23,8 @@ mixin SourceBase {
|
||||||
|
|
||||||
List<AvesEntry> get sortedEntriesByDate;
|
List<AvesEntry> get sortedEntriesByDate;
|
||||||
|
|
||||||
|
ValueNotifier<SourceState> stateNotifier = ValueNotifier(SourceState.ready);
|
||||||
|
|
||||||
final StreamController<ProgressEvent> _progressStreamController = StreamController.broadcast();
|
final StreamController<ProgressEvent> _progressStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
Stream<ProgressEvent> get progressStream => _progressStreamController.stream;
|
Stream<ProgressEvent> get progressStream => _progressStreamController.stream;
|
||||||
|
@ -58,8 +60,6 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
return _sortedEntriesByDate;
|
return _sortedEntriesByDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
ValueNotifier<SourceState> stateNotifier = ValueNotifier(SourceState.ready);
|
|
||||||
|
|
||||||
List<DateMetadata> _savedDates;
|
List<DateMetadata> _savedDates;
|
||||||
|
|
||||||
Future<void> loadDates() async {
|
Future<void> loadDates() async {
|
||||||
|
|
|
@ -35,9 +35,14 @@ mixin LocationMixin on SourceBase {
|
||||||
|
|
||||||
// quick reverse geocoding to find the countries, using an offline asset
|
// quick reverse geocoding to find the countries, using an offline asset
|
||||||
Future<void> _locateCountries() async {
|
Future<void> _locateCountries() async {
|
||||||
final todo = visibleEntries.where((entry) => entry.hasGps && entry.addressDetails?.countryCode == null).toSet();
|
final todo = visibleEntries.where((entry) => entry.hasGps && !entry.hasAddress).toSet();
|
||||||
if (todo.isEmpty) return;
|
if (todo.isEmpty) return;
|
||||||
|
|
||||||
|
stateNotifier.value = SourceState.locating;
|
||||||
|
var progressDone = 0;
|
||||||
|
final progressTotal = todo.length;
|
||||||
|
setProgress(done: progressDone, total: progressTotal);
|
||||||
|
|
||||||
// final stopwatch = Stopwatch()..start();
|
// final stopwatch = Stopwatch()..start();
|
||||||
final countryCodeMap = await countryTopology.countryCodeMap(todo.map((entry) => entry.latLng).toSet());
|
final countryCodeMap = await countryTopology.countryCodeMap(todo.map((entry) => entry.latLng).toSet());
|
||||||
final newAddresses = <AddressDetails>[];
|
final newAddresses = <AddressDetails>[];
|
||||||
|
@ -48,12 +53,13 @@ mixin LocationMixin on SourceBase {
|
||||||
if (entry.hasAddress) {
|
if (entry.hasAddress) {
|
||||||
newAddresses.add(entry.addressDetails);
|
newAddresses.add(entry.addressDetails);
|
||||||
}
|
}
|
||||||
|
setProgress(done: ++progressDone, total: progressTotal);
|
||||||
});
|
});
|
||||||
if (newAddresses.isNotEmpty) {
|
if (newAddresses.isNotEmpty) {
|
||||||
await metadataDb.saveAddresses(List.unmodifiable(newAddresses));
|
await metadataDb.saveAddresses(List.unmodifiable(newAddresses));
|
||||||
onAddressMetadataChanged();
|
onAddressMetadataChanged();
|
||||||
}
|
}
|
||||||
// debugPrint('$runtimeType _locateCountries complete in ${stopwatch.elapsed.inSeconds}s');
|
// debugPrint('$runtimeType _locateCountries complete in ${stopwatch.elapsed.inMilliseconds}ms');
|
||||||
}
|
}
|
||||||
|
|
||||||
// full reverse geocoding, requiring Play Services and some connectivity
|
// full reverse geocoding, requiring Play Services and some connectivity
|
||||||
|
@ -61,7 +67,7 @@ mixin LocationMixin on SourceBase {
|
||||||
if (!(await availability.canLocatePlaces)) return;
|
if (!(await availability.canLocatePlaces)) return;
|
||||||
|
|
||||||
// final stopwatch = Stopwatch()..start();
|
// final stopwatch = Stopwatch()..start();
|
||||||
final byLocated = groupBy<AvesEntry, bool>(visibleEntries.where((entry) => entry.hasGps), (entry) => entry.hasPlace);
|
final byLocated = groupBy<AvesEntry, bool>(visibleEntries.where((entry) => entry.hasGps), (entry) => entry.hasFineAddress);
|
||||||
final todo = byLocated[false] ?? [];
|
final todo = byLocated[false] ?? [];
|
||||||
if (todo.isEmpty) return;
|
if (todo.isEmpty) return;
|
||||||
|
|
||||||
|
@ -85,6 +91,7 @@ mixin LocationMixin on SourceBase {
|
||||||
final knownLocations = <Tuple2, AddressDetails>{};
|
final knownLocations = <Tuple2, AddressDetails>{};
|
||||||
byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails));
|
byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails));
|
||||||
|
|
||||||
|
stateNotifier.value = SourceState.locating;
|
||||||
var progressDone = 0;
|
var progressDone = 0;
|
||||||
final progressTotal = todo.length;
|
final progressTotal = todo.length;
|
||||||
setProgress(done: progressDone, total: progressTotal);
|
setProgress(done: progressDone, total: progressTotal);
|
||||||
|
@ -100,7 +107,7 @@ mixin LocationMixin on SourceBase {
|
||||||
// so that we skip geocoding of following entries with the same coordinates
|
// so that we skip geocoding of following entries with the same coordinates
|
||||||
knownLocations[latLng] = entry.addressDetails;
|
knownLocations[latLng] = entry.addressDetails;
|
||||||
}
|
}
|
||||||
if (entry.hasPlace) {
|
if (entry.hasFineAddress) {
|
||||||
newAddresses.add(entry.addressDetails);
|
newAddresses.add(entry.addressDetails);
|
||||||
if (newAddresses.length >= _commitCountThreshold) {
|
if (newAddresses.length >= _commitCountThreshold) {
|
||||||
await metadataDb.saveAddresses(List.unmodifiable(newAddresses));
|
await metadataDb.saveAddresses(List.unmodifiable(newAddresses));
|
||||||
|
@ -147,7 +154,8 @@ mixin LocationMixin on SourceBase {
|
||||||
_filterEntryCountMap.clear();
|
_filterEntryCountMap.clear();
|
||||||
_filterRecentEntryMap.clear();
|
_filterRecentEntryMap.clear();
|
||||||
} else {
|
} else {
|
||||||
final countryCodes = entries.where((entry) => entry.hasPlace).map((entry) => entry.addressDetails.countryCode).toSet();
|
final countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails.countryCode).toSet();
|
||||||
|
countryCodes.remove(null);
|
||||||
countryCodes.forEach(_filterEntryCountMap.remove);
|
countryCodes.forEach(_filterEntryCountMap.remove);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
|
||||||
|
|
||||||
class MediaStoreSource extends CollectionSource {
|
class MediaStoreSource extends CollectionSource {
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
@ -103,25 +102,25 @@ class MediaStoreSource extends CollectionSource {
|
||||||
updateDirectories();
|
updateDirectories();
|
||||||
}
|
}
|
||||||
|
|
||||||
final analytics = FirebaseAnalytics();
|
|
||||||
unawaited(analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(allEntries.length, 3)).toString()));
|
|
||||||
unawaited(analytics.setUserProperty(name: 'album_count', value: (ceilBy(rawAlbums.length, 1)).toString()));
|
|
||||||
|
|
||||||
stateNotifier.value = SourceState.cataloguing;
|
|
||||||
await catalogEntries();
|
await catalogEntries();
|
||||||
unawaited(analytics.setUserProperty(name: 'tag_count', value: (ceilBy(sortedTags.length, 1)).toString()));
|
|
||||||
|
|
||||||
stateNotifier.value = SourceState.locating;
|
|
||||||
await locateEntries();
|
await locateEntries();
|
||||||
unawaited(analytics.setUserProperty(name: 'country_count', value: (ceilBy(sortedCountries.length, 1)).toString()));
|
|
||||||
|
|
||||||
stateNotifier.value = SourceState.ready;
|
stateNotifier.value = SourceState.ready;
|
||||||
|
|
||||||
|
_reportCollectionDimensions();
|
||||||
debugPrint('$runtimeType refresh done, elapsed=${stopwatch.elapsed}');
|
debugPrint('$runtimeType refresh done, elapsed=${stopwatch.elapsed}');
|
||||||
},
|
},
|
||||||
onError: (error) => debugPrint('$runtimeType stream error=$error'),
|
onError: (error) => debugPrint('$runtimeType stream error=$error'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _reportCollectionDimensions() {
|
||||||
|
final analytics = FirebaseAnalytics();
|
||||||
|
analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(allEntries.length, 3)).toString());
|
||||||
|
analytics.setUserProperty(name: 'album_count', value: (ceilBy(rawAlbums.length, 1)).toString());
|
||||||
|
analytics.setUserProperty(name: 'tag_count', value: (ceilBy(sortedTags.length, 1)).toString());
|
||||||
|
analytics.setUserProperty(name: 'country_count', value: (ceilBy(sortedCountries.length, 1)).toString());
|
||||||
|
}
|
||||||
|
|
||||||
// returns URIs to retry later. They could be URIs that are:
|
// returns URIs to retry later. They could be URIs that are:
|
||||||
// 1) currently being processed during bulk move/deletion
|
// 1) currently being processed during bulk move/deletion
|
||||||
// 2) registered in the Media Store but still being processed by their owner in a temporary location
|
// 2) registered in the Media Store but still being processed by their owner in a temporary location
|
||||||
|
@ -178,13 +177,8 @@ class MediaStoreSource extends CollectionSource {
|
||||||
addEntries(newEntries);
|
addEntries(newEntries);
|
||||||
await metadataDb.saveEntries(newEntries);
|
await metadataDb.saveEntries(newEntries);
|
||||||
cleanEmptyAlbums(existingDirectories);
|
cleanEmptyAlbums(existingDirectories);
|
||||||
|
|
||||||
stateNotifier.value = SourceState.cataloguing;
|
|
||||||
await catalogEntries();
|
await catalogEntries();
|
||||||
|
|
||||||
stateNotifier.value = SourceState.locating;
|
|
||||||
await locateEntries();
|
await locateEntries();
|
||||||
|
|
||||||
stateNotifier.value = SourceState.ready;
|
stateNotifier.value = SourceState.ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ mixin TagMixin on SourceBase {
|
||||||
final todo = visibleEntries.where((entry) => !entry.isCatalogued).toList();
|
final todo = visibleEntries.where((entry) => !entry.isCatalogued).toList();
|
||||||
if (todo.isEmpty) return;
|
if (todo.isEmpty) return;
|
||||||
|
|
||||||
|
stateNotifier.value = SourceState.cataloguing;
|
||||||
var progressDone = 0;
|
var progressDone = 0;
|
||||||
final progressTotal = todo.length;
|
final progressTotal = todo.length;
|
||||||
setProgress(done: progressDone, total: progressTotal);
|
setProgress(done: progressDone, total: progressTotal);
|
||||||
|
|
|
@ -18,6 +18,7 @@ class MimeTypes {
|
||||||
static const cr2 = 'image/x-canon-cr2';
|
static const cr2 = 'image/x-canon-cr2';
|
||||||
static const crw = 'image/x-canon-crw';
|
static const crw = 'image/x-canon-crw';
|
||||||
static const dcr = 'image/x-kodak-dcr';
|
static const dcr = 'image/x-kodak-dcr';
|
||||||
|
static const djvu = 'image/vnd.djvu';
|
||||||
static const dng = 'image/x-adobe-dng';
|
static const dng = 'image/x-adobe-dng';
|
||||||
static const erf = 'image/x-epson-erf';
|
static const erf = 'image/x-epson-erf';
|
||||||
static const k25 = 'image/x-kodak-k25';
|
static const k25 = 'image/x-kodak-k25';
|
||||||
|
|
|
@ -28,7 +28,7 @@ class AndroidDebugService {
|
||||||
|
|
||||||
static Future<Map> getBitmapFactoryInfo(AvesEntry entry) async {
|
static Future<Map> getBitmapFactoryInfo(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with all data available when decoding image bounds with `BitmapFactory`
|
// returns map with all data available when decoding image bounds with `BitmapFactory`
|
||||||
final result = await platform.invokeMethod('getBitmapFactoryInfo', <String, dynamic>{
|
final result = await platform.invokeMethod('getBitmapFactoryInfo', <String, dynamic>{
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
|
@ -41,7 +41,7 @@ class AndroidDebugService {
|
||||||
|
|
||||||
static Future<Map> getContentResolverMetadata(AvesEntry entry) async {
|
static Future<Map> getContentResolverMetadata(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with all data available from the content resolver
|
// returns map with all data available from the content resolver
|
||||||
final result = await platform.invokeMethod('getContentResolverMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getContentResolverMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
@ -55,7 +55,7 @@ class AndroidDebugService {
|
||||||
|
|
||||||
static Future<Map> getExifInterfaceMetadata(AvesEntry entry) async {
|
static Future<Map> getExifInterfaceMetadata(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with all data available from the `ExifInterface` library
|
// returns map with all data available from the `ExifInterface` library
|
||||||
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
@ -70,7 +70,7 @@ class AndroidDebugService {
|
||||||
|
|
||||||
static Future<Map> getMediaMetadataRetrieverMetadata(AvesEntry entry) async {
|
static Future<Map> getMediaMetadataRetrieverMetadata(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with all data available from `MediaMetadataRetriever`
|
// returns map with all data available from `MediaMetadataRetriever`
|
||||||
final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', <String, dynamic>{
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
|
@ -83,7 +83,7 @@ class AndroidDebugService {
|
||||||
|
|
||||||
static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async {
|
static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with the mime type and tag count for each directory found by `metadata-extractor`
|
// returns map with the mime type and tag count for each directory found by `metadata-extractor`
|
||||||
final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
|
final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
|
|
@ -95,7 +95,7 @@ class AndroidFileService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return media URI
|
// returns media URI
|
||||||
static Future<Uri> scanFile(String path, String mimeType) async {
|
static Future<Uri> scanFile(String path, String mimeType) async {
|
||||||
debugPrint('scanFile with path=$path, mimeType=$mimeType');
|
debugPrint('scanFile with path=$path, mimeType=$mimeType');
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -248,7 +248,7 @@ class ImageFileService {
|
||||||
|
|
||||||
static Future<Map> rename(AvesEntry entry, String newName) async {
|
static Future<Map> rename(AvesEntry entry, String newName) async {
|
||||||
try {
|
try {
|
||||||
// return map with: 'contentId' 'path' 'title' 'uri' (all optional)
|
// returns map with: 'contentId' 'path' 'title' 'uri' (all optional)
|
||||||
final result = await platform.invokeMethod('rename', <String, dynamic>{
|
final result = await platform.invokeMethod('rename', <String, dynamic>{
|
||||||
'entry': _toPlatformEntryMap(entry),
|
'entry': _toPlatformEntryMap(entry),
|
||||||
'newName': newName,
|
'newName': newName,
|
||||||
|
@ -262,7 +262,7 @@ class ImageFileService {
|
||||||
|
|
||||||
static Future<Map> rotate(AvesEntry entry, {@required bool clockwise}) async {
|
static Future<Map> rotate(AvesEntry entry, {@required bool clockwise}) async {
|
||||||
try {
|
try {
|
||||||
// return map with: 'rotationDegrees' 'isFlipped'
|
// returns map with: 'rotationDegrees' 'isFlipped'
|
||||||
final result = await platform.invokeMethod('rotate', <String, dynamic>{
|
final result = await platform.invokeMethod('rotate', <String, dynamic>{
|
||||||
'entry': _toPlatformEntryMap(entry),
|
'entry': _toPlatformEntryMap(entry),
|
||||||
'clockwise': clockwise,
|
'clockwise': clockwise,
|
||||||
|
@ -276,7 +276,7 @@ class ImageFileService {
|
||||||
|
|
||||||
static Future<Map> flip(AvesEntry entry) async {
|
static Future<Map> flip(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with: 'rotationDegrees' 'isFlipped'
|
// returns map with: 'rotationDegrees' 'isFlipped'
|
||||||
final result = await platform.invokeMethod('flip', <String, dynamic>{
|
final result = await platform.invokeMethod('flip', <String, dynamic>{
|
||||||
'entry': _toPlatformEntryMap(entry),
|
'entry': _toPlatformEntryMap(entry),
|
||||||
}) as Map;
|
}) as Map;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import 'package:flutter/services.dart';
|
||||||
class MetadataService {
|
class MetadataService {
|
||||||
static const platform = MethodChannel('deckers.thibault/aves/metadata');
|
static const platform = MethodChannel('deckers.thibault/aves/metadata');
|
||||||
|
|
||||||
// return Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
|
// returns Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
|
||||||
static Future<Map> getAllMetadata(AvesEntry entry) async {
|
static Future<Map> getAllMetadata(AvesEntry entry) async {
|
||||||
if (entry.isSvg) return null;
|
if (entry.isSvg) return null;
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class MetadataService {
|
||||||
|
|
||||||
Future<CatalogMetadata> call() async {
|
Future<CatalogMetadata> call() async {
|
||||||
try {
|
try {
|
||||||
// return map with:
|
// returns map with:
|
||||||
// 'mimeType': MIME type as reported by metadata extractors, not Media Store (string)
|
// 'mimeType': MIME type as reported by metadata extractors, not Media Store (string)
|
||||||
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
// 'dateMillis': date taken in milliseconds since Epoch (long)
|
||||||
// 'isAnimated': animated gif/webp (bool)
|
// 'isAnimated': animated gif/webp (bool)
|
||||||
|
@ -69,7 +69,7 @@ class MetadataService {
|
||||||
if (entry.isSvg) return null;
|
if (entry.isSvg) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// return map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int)
|
// returns map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int)
|
||||||
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
@ -98,7 +98,7 @@ class MetadataService {
|
||||||
|
|
||||||
static Future<PanoramaInfo> getPanoramaInfo(AvesEntry entry) async {
|
static Future<PanoramaInfo> getPanoramaInfo(AvesEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with values for:
|
// returns map with values for:
|
||||||
// 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int),
|
// 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int),
|
||||||
// 'fullPanoWidth' (int), 'fullPanoHeight' (int)
|
// 'fullPanoWidth' (int), 'fullPanoHeight' (int)
|
||||||
final result = await platform.invokeMethod('getPanoramaInfo', <String, dynamic>{
|
final result = await platform.invokeMethod('getPanoramaInfo', <String, dynamic>{
|
||||||
|
|
|
@ -6,7 +6,7 @@ class ViewerService {
|
||||||
|
|
||||||
static Future<Map> getIntentData() async {
|
static Future<Map> getIntentData() async {
|
||||||
try {
|
try {
|
||||||
// return nullable map with 'action' and possibly 'uri' 'mimeType'
|
// returns nullable map with 'action' and possibly 'uri' 'mimeType'
|
||||||
return await platform.invokeMethod('getIntentData') as Map;
|
return await platform.invokeMethod('getIntentData') as Map;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
|
|
|
@ -64,7 +64,7 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
||||||
final catalogued = visibleEntries.where((entry) => entry.isCatalogued);
|
final catalogued = visibleEntries.where((entry) => entry.isCatalogued);
|
||||||
final withGps = catalogued.where((entry) => entry.hasGps);
|
final withGps = catalogued.where((entry) => entry.hasGps);
|
||||||
final withAddress = withGps.where((entry) => entry.hasAddress);
|
final withAddress = withGps.where((entry) => entry.hasAddress);
|
||||||
final withPlace = withGps.where((entry) => entry.hasPlace);
|
final withFineAddress = withGps.where((entry) => entry.hasFineAddress);
|
||||||
return AvesExpansionTile(
|
return AvesExpansionTile(
|
||||||
title: 'General',
|
title: 'General',
|
||||||
children: [
|
children: [
|
||||||
|
@ -106,7 +106,7 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
||||||
'Catalogued': '${catalogued.length}',
|
'Catalogued': '${catalogued.length}',
|
||||||
'With GPS': '${withGps.length}',
|
'With GPS': '${withGps.length}',
|
||||||
'With address': '${withAddress.length}',
|
'With address': '${withAddress.length}',
|
||||||
'With place': '${withPlace.length}',
|
'With fine address': '${withFineAddress.length}',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -107,7 +107,7 @@ class ViewerDebugPage extends StatelessWidget {
|
||||||
InfoRowGroup({
|
InfoRowGroup({
|
||||||
'hasGps': '${entry.hasGps}',
|
'hasGps': '${entry.hasGps}',
|
||||||
'hasAddress': '${entry.hasAddress}',
|
'hasAddress': '${entry.hasAddress}',
|
||||||
'hasPlace': '${entry.hasPlace}',
|
'hasFineAddress': '${entry.hasFineAddress}',
|
||||||
'latLng': '${entry.latLng}',
|
'latLng': '${entry.latLng}',
|
||||||
'geoUri': '${entry.geoUri}',
|
'geoUri': '${entry.geoUri}',
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -151,7 +151,7 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
||||||
// make sure to locate the entry,
|
// make sure to locate the entry,
|
||||||
// so that we can display the address instead of coordinates
|
// so that we can display the address instead of coordinates
|
||||||
// even when initial collection locating has not reached this entry yet
|
// even when initial collection locating has not reached this entry yet
|
||||||
entry.locate();
|
entry.catalog(background: false).then((_) => entry.locate(background: false));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ class BasicSection extends StatelessWidget {
|
||||||
'Title': title,
|
'Title': title,
|
||||||
'Date': dateText,
|
'Date': dateText,
|
||||||
if (entry.isVideo) ..._buildVideoRows(),
|
if (entry.isVideo) ..._buildVideoRows(),
|
||||||
if (!entry.isSvg) 'Resolution': rasterResolutionText,
|
if (!entry.isSvg && entry.isSized) 'Resolution': rasterResolutionText,
|
||||||
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.infoUnknown,
|
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.infoUnknown,
|
||||||
'URI': uri,
|
'URI': uri,
|
||||||
if (path != null) 'Path': path,
|
if (path != null) 'Path': path,
|
||||||
|
|
|
@ -386,7 +386,7 @@ class _DateRow extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final date = entry.bestDate;
|
final date = entry.bestDate;
|
||||||
final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.overlayUnknown;
|
final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.overlayUnknown;
|
||||||
final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.resolutionText;
|
final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.isSized ? entry.resolutionText : '';
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
Loading…
Reference in a new issue