entry: split change notifier, address fallback

This commit is contained in:
Thibault Deckers 2019-09-07 16:13:43 +09:00
parent 31eb61433e
commit ff34e77cb3
11 changed files with 115 additions and 27 deletions

View file

@ -120,7 +120,7 @@ class ImageCollection with ChangeNotifier {
catalogEntries() async {
final start = DateTime.now();
final uncataloguedEntries = _rawEntries.where((entry) => !entry.isCatalogued);
final uncataloguedEntries = _rawEntries.where((entry) => !entry.isCatalogued).toList();
final newMetadata = List<CatalogMetadata>();
await Future.forEach<ImageEntry>(uncataloguedEntries, (entry) async {
await entry.catalog();
@ -133,7 +133,7 @@ class ImageCollection with ChangeNotifier {
locateEntries() async {
final start = DateTime.now();
final unlocatedEntries = _rawEntries.where((entry) => entry.hasGps && !entry.isLocated);
final unlocatedEntries = _rawEntries.where((entry) => entry.hasGps && !entry.isLocated).toList();
final newAddresses = List<AddressDetails>();
await Future.forEach<ImageEntry>(unlocatedEntries, (entry) async {
await entry.locate();

View file

@ -3,6 +3,7 @@ import 'dart:collection';
import 'package:aves/model/image_file_service.dart';
import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_service.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:flutter/foundation.dart';
import 'package:geocoder/geocoder.dart';
import 'package:path/path.dart';
@ -10,7 +11,7 @@ import 'package:tuple/tuple.dart';
import 'mime_types.dart';
class ImageEntry with ChangeNotifier {
class ImageEntry {
String uri;
String path;
String directory;
@ -28,6 +29,8 @@ class ImageEntry with ChangeNotifier {
CatalogMetadata catalogMetadata;
AddressDetails addressDetails;
AChangeNotifier imageChangeNotifier = new AChangeNotifier(), metadataChangeNotifier = new AChangeNotifier(), addressChangeNotifier = new AChangeNotifier();
ImageEntry({
this.uri,
this.path,
@ -80,6 +83,12 @@ class ImageEntry with ChangeNotifier {
};
}
dispose() {
imageChangeNotifier.dispose();
metadataChangeNotifier.dispose();
addressChangeNotifier.dispose();
}
@override
String toString() {
return 'ImageEntry{uri=$uri, path=$path}';
@ -143,7 +152,7 @@ class ImageEntry with ChangeNotifier {
catalog() async {
if (isCatalogued) return;
catalogMetadata = await MetadataService.getCatalogMetadata(this);
notifyListeners();
metadataChangeNotifier.notifyListeners();
}
locate() async {
@ -166,10 +175,10 @@ class ImageEntry with ChangeNotifier {
adminArea: address.adminArea,
locality: address.locality,
);
notifyListeners();
addressChangeNotifier.notifyListeners();
}
} catch (e) {
debugPrint('$runtimeType addAddressToMetadata failed with exception=${e.message}');
} catch (exception) {
debugPrint('$runtimeType addAddressToMetadata failed with exception=$exception');
}
}
@ -204,7 +213,7 @@ class ImageEntry with ChangeNotifier {
if (contentId != null) this.contentId = contentId;
final title = newFields['title'];
if (title != null) this.title = title;
notifyListeners();
metadataChangeNotifier.notifyListeners();
return true;
}
@ -220,7 +229,7 @@ class ImageEntry with ChangeNotifier {
if (height != null) this.height = height;
final orientationDegrees = newFields['orientationDegrees'];
if (orientationDegrees != null) this.orientationDegrees = orientationDegrees;
notifyListeners();
imageChangeNotifier.notifyListeners();
return true;
}
}

View file

@ -5,16 +5,16 @@ final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
typedef void AndroidFileUtilsCallback(String key, dynamic oldValue, dynamic newValue);
class AndroidFileUtils {
String dcimPath, downloadPath, picturesPath;
String externalStorage, dcimPath, downloadPath, picturesPath;
AndroidFileUtils._private();
init() async {
// path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files'
final ext = '/storage/emulated/0';
dcimPath = join(ext, 'DCIM');
downloadPath = join(ext, 'Download');
picturesPath = join(ext, 'Pictures');
externalStorage = '/storage/emulated/0';
dcimPath = join(externalStorage, 'DCIM');
downloadPath = join(externalStorage, 'Download');
picturesPath = join(externalStorage, 'Pictures');
}
bool isCameraPath(String path) => path != null && path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO'));

View file

@ -0,0 +1,27 @@
import 'package:flutter/foundation.dart';
// reimplemented ChangeNotifier so that it can be used anywhere, not just as a mixin
class AChangeNotifier implements Listenable {
ObserverList<VoidCallback> _listeners = ObserverList<VoidCallback>();
@override
void addListener(VoidCallback listener) => _listeners.add(listener);
@override
void removeListener(VoidCallback listener) => _listeners.remove(listener);
void dispose() => _listeners = null;
@protected
void notifyListeners() {
if (_listeners == null) return;
final localListeners = List<VoidCallback>.from(_listeners);
for (final listener in localListeners) {
try {
if (_listeners.contains(listener)) listener();
} catch (exception, stack) {
debugPrint('$runtimeType failed to notify listeners with exception=$exception\n$stack');
}
}
}
}

38
lib/utils/geo_utils.dart Normal file
View file

@ -0,0 +1,38 @@
import 'dart:math' as math;
import 'package:intl/intl.dart';
import 'package:tuple/tuple.dart';
// adapted from Mike Mitterer's dart-latlong library
String _decimal2sexagesimal(final double dec) {
double _round(final double value, {final int decimals: 6}) => (value * math.pow(10, decimals)).round() / math.pow(10, decimals);
List<int> _split(final double value) {
// NumberFormat is necessary to create digit after comma if the value
// has no decimal point (only necessary for browser)
final List<String> tmp = new NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.');
return <int>[int.parse(tmp[0]).abs(), int.parse(tmp[1])];
}
final List<int> parts = _split(dec);
final int integerPart = parts[0];
final int fractionalPart = parts[1];
final int deg = integerPart;
final double min = double.parse('0.$fractionalPart') * 60;
final List<int> minParts = _split(min);
final int minFractionalPart = minParts[1];
final double sec = (double.parse('0.$minFractionalPart') * 60);
return '$deg° ${min.floor()} ${_round(sec, decimals: 2).toStringAsFixed(2)}';
}
// return coordinates formatted as DMS, e.g. ['41°2412.2″ N', '2°1026.5″E']
List<String> toDMS(Tuple2<double, double> latLng) {
if (latLng == null) return [];
final lat = latLng.item1;
final lng = latLng.item2;
return ['${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}', '${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}'];
}

View file

@ -25,6 +25,7 @@ class Thumbnail extends StatefulWidget {
class ThumbnailState extends State<Thumbnail> {
Future<Uint8List> _byteLoader;
Listenable _entryChangeNotifier;
ImageEntry get entry => widget.entry;
@ -33,7 +34,8 @@ class ThumbnailState extends State<Thumbnail> {
@override
void initState() {
super.initState();
entry.addListener(onEntryChange);
_entryChangeNotifier = Listenable.merge([entry.imageChangeNotifier, entry.metadataChangeNotifier]);
_entryChangeNotifier.addListener(onEntryChange);
initByteLoader();
}
@ -53,7 +55,7 @@ class ThumbnailState extends State<Thumbnail> {
@override
void dispose() {
entry.removeListener(onEntryChange);
_entryChangeNotifier.removeListener(onEntryChange);
super.dispose();
}

View file

@ -99,7 +99,7 @@ class IconUtils {
if (androidFileUtils.isDownloadPath(albumDirectory)) return Icon(Icons.file_download);
final parts = albumDirectory.split(separator);
if (albumDirectory.startsWith(androidFileUtils.picturesPath) && appNameMap.keys.contains(parts.last)) {
if (albumDirectory.startsWith(androidFileUtils.externalStorage) && appNameMap.keys.contains(parts.last)) {
final packageName = appNameMap[parts.last];
return AppIcon(
packageName: packageName,

View file

@ -9,11 +9,11 @@ class LocationSection extends AnimatedWidget {
final ImageEntry entry;
final showTitle;
const LocationSection({
LocationSection({
Key key,
@required this.entry,
@required this.showTitle,
}) : super(key: key, listenable: entry);
}) : super(key: key, listenable: Listenable.merge([entry.metadataChangeNotifier, entry.addressChangeNotifier]));
@override
Widget build(BuildContext context) {

View file

@ -8,11 +8,11 @@ class XmpTagSection extends AnimatedWidget {
final ImageCollection collection;
final ImageEntry entry;
const XmpTagSection({
XmpTagSection({
Key key,
@required this.collection,
@required this.entry,
}) : super(key: key, listenable: entry);
}) : super(key: key, listenable: entry.metadataChangeNotifier);
@override
Widget build(BuildContext context) {

View file

@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_service.dart';
import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/blurred.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
@ -123,7 +124,7 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
width: maxWidth,
child: Text('$position ${entry.title}', overflow: TextOverflow.ellipsis),
),
if (entry.isLocated)
if (entry.hasGps)
Container(
padding: EdgeInsets.only(top: interRowPadding),
width: subRowWidth,
@ -158,11 +159,17 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
}
Widget _buildLocationRow() {
String text;
if (entry.isLocated) {
text = entry.shortAddress;
} else if (entry.hasGps) {
text = toDMS(entry.latLng).join(', ');
}
return Row(
children: [
Icon(Icons.place, size: iconSize),
SizedBox(width: iconPadding),
Expanded(child: Text(entry.shortAddress, overflow: TextOverflow.ellipsis)),
Expanded(child: Text(text, overflow: TextOverflow.ellipsis)),
],
);
}

View file

@ -45,6 +45,15 @@ class FullscreenTopOverlay extends StatelessWidget {
),
),
SizedBox(width: 8),
OverlayButton(
scale: scale,
child: IconButton(
icon: Icon(Icons.delete_outline),
onPressed: () => onActionSelected?.call(FullscreenAction.delete),
tooltip: 'Delete',
),
),
SizedBox(width: 8),
OverlayButton(
scale: scale,
child: PopupMenuButton<FullscreenAction>(
@ -53,10 +62,6 @@ class FullscreenTopOverlay extends StatelessWidget {
value: FullscreenAction.info,
child: MenuRow(text: 'Info', icon: Icons.info_outline),
),
PopupMenuItem(
value: FullscreenAction.delete,
child: MenuRow(text: 'Delete', icon: Icons.delete_outline),
),
PopupMenuItem(
value: FullscreenAction.rename,
child: MenuRow(text: 'Rename', icon: Icons.title),