entry: split change notifier, address fallback
This commit is contained in:
parent
31eb61433e
commit
ff34e77cb3
11 changed files with 115 additions and 27 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
|
|
27
lib/utils/change_notifier.dart
Normal file
27
lib/utils/change_notifier.dart
Normal 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
38
lib/utils/geo_utils.dart
Normal 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°24′12.2″ N', '2°10′26.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'}'];
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue