aves/lib/model/image_entry.dart

230 lines
7.2 KiB
Dart

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:aves/utils/time_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:geocoder/geocoder.dart';
import 'package:path/path.dart';
import 'package:tuple/tuple.dart';
import 'mime_types.dart';
class ImageEntry {
String uri;
String path;
String directory;
int contentId;
final String mimeType;
int width;
int height;
int orientationDegrees;
final int sizeBytes;
String title;
final int dateModifiedSecs;
final int sourceDateTakenMillis;
final String bucketDisplayName;
final int durationMillis;
CatalogMetadata catalogMetadata;
AddressDetails addressDetails;
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
ImageEntry({
this.uri,
this.path,
this.contentId,
this.mimeType,
this.width,
this.height,
this.orientationDegrees,
this.sizeBytes,
this.title,
this.dateModifiedSecs,
this.sourceDateTakenMillis,
this.bucketDisplayName,
this.durationMillis,
}) : directory = path != null ? dirname(path) : null;
factory ImageEntry.fromMap(Map map) {
return ImageEntry(
uri: map['uri'],
path: map['path'],
contentId: map['contentId'],
mimeType: map['mimeType'],
width: map['width'],
height: map['height'],
orientationDegrees: map['orientationDegrees'],
sizeBytes: map['sizeBytes'],
title: map['title'],
dateModifiedSecs: map['dateModifiedSecs'],
sourceDateTakenMillis: map['sourceDateTakenMillis'],
bucketDisplayName: map['bucketDisplayName'],
durationMillis: map['durationMillis'],
);
}
Map<String, dynamic> toMap() {
return {
'uri': uri,
'path': path,
'contentId': contentId,
'mimeType': mimeType,
'width': width,
'height': height,
'orientationDegrees': orientationDegrees,
'sizeBytes': sizeBytes,
'title': title,
'dateModifiedSecs': dateModifiedSecs,
'sourceDateTakenMillis': sourceDateTakenMillis,
'bucketDisplayName': bucketDisplayName,
'durationMillis': durationMillis,
};
}
void dispose() {
imageChangeNotifier.dispose();
metadataChangeNotifier.dispose();
addressChangeNotifier.dispose();
}
@override
String toString() {
return 'ImageEntry{uri=$uri, path=$path}';
}
String get filename => basenameWithoutExtension(path);
bool get isGif => mimeType == MimeTypes.MIME_GIF;
bool get isVideo => mimeType.startsWith(MimeTypes.MIME_VIDEO);
bool get isCatalogued => catalogMetadata != null;
double get aspectRatio {
if (width == 0 || height == 0) return 1;
if (isVideo && isCatalogued) {
if (catalogMetadata.videoRotation % 180 == 90) return height / width;
}
return width / height;
}
int get megaPixels => (width * height / 1000000).round();
DateTime get bestDate {
if ((catalogMetadata?.dateMillis ?? 0) > 0) return DateTime.fromMillisecondsSinceEpoch(catalogMetadata.dateMillis);
if (sourceDateTakenMillis != null && sourceDateTakenMillis > 0) return DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis);
if (dateModifiedSecs != null && dateModifiedSecs > 0) return DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000);
return null;
}
DateTime get monthTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month);
}
DateTime get dayTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month, d.day);
}
String get durationText => formatDuration(Duration(milliseconds: durationMillis));
bool get hasGps => isCatalogued && catalogMetadata.latitude != null;
bool get isLocated => addressDetails != null;
Tuple2<double, double> get latLng => isCatalogued ? Tuple2(catalogMetadata.latitude, catalogMetadata.longitude) : null;
String get geoUri => hasGps ? 'geo:${catalogMetadata.latitude},${catalogMetadata.longitude}' : null;
List<String> get xmpSubjects => catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? [];
Future<void> catalog() async {
if (isCatalogued) return;
catalogMetadata = await MetadataService.getCatalogMetadata(this);
metadataChangeNotifier.notifyListeners();
}
Future<void> locate() async {
if (isLocated) return;
await catalog();
final latitude = catalogMetadata?.latitude;
final longitude = catalogMetadata?.longitude;
if (latitude == null || longitude == null) return;
final coordinates = Coordinates(latitude, longitude);
try {
final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates);
if (addresses != null && addresses.isNotEmpty) {
final address = addresses.first;
addressDetails = AddressDetails(
contentId: contentId,
addressLine: address.addressLine,
countryName: address.countryName,
adminArea: address.adminArea,
locality: address.locality,
);
addressChangeNotifier.notifyListeners();
}
} catch (exception) {
debugPrint('$runtimeType addAddressToMetadata failed with exception=$exception');
}
}
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) {
if (title.toLowerCase().contains(query)) return true;
if (catalogMetadata?.xmpSubjects?.toLowerCase()?.contains(query) ?? false) return true;
if (isLocated && addressDetails.addressLine.toLowerCase().contains(query)) return true;
return false;
}
Future<bool> rename(String newName) async {
if (newName == filename) return true;
final newFields = await ImageFileService.rename(this, '$newName${extension(this.path)}');
if (newFields.isEmpty) return false;
final uri = newFields['uri'];
if (uri != null) this.uri = uri;
final path = newFields['path'];
if (path != null) this.path = path;
final contentId = newFields['contentId'];
if (contentId != null) this.contentId = contentId;
final title = newFields['title'];
if (title != null) this.title = title;
metadataChangeNotifier.notifyListeners();
return true;
}
bool get canPrint => !isVideo;
bool get canRotate => mimeType == MimeTypes.MIME_JPEG || mimeType == MimeTypes.MIME_PNG;
Future<bool> rotate({@required bool clockwise}) async {
final newFields = await ImageFileService.rotate(this, clockwise: clockwise);
if (newFields.isEmpty) return false;
final width = newFields['width'];
if (width != null) this.width = width;
final height = newFields['height'];
if (height != null) this.height = height;
final orientationDegrees = newFields['orientationDegrees'];
if (orientationDegrees != null) this.orientationDegrees = orientationDegrees;
imageChangeNotifier.notifyListeners();
return true;
}
}