refactor
This commit is contained in:
parent
0120efc800
commit
5784607130
191 changed files with 2051 additions and 677 deletions
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/vaults/vaults.dart';
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/metadata/address.dart';
|
import 'package:aves/model/metadata/address.dart';
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:io';
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/db/db_metadata.dart';
|
import 'package:aves/model/db/db_metadata.dart';
|
||||||
import 'package:aves/model/db/db_metadata_sqflite_upgrade.dart';
|
import 'package:aves/model/db/db_metadata_sqflite_upgrade.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/metadata/address.dart';
|
import 'package:aves/model/metadata/address.dart';
|
||||||
|
|
|
@ -2,51 +2,42 @@ import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/geo/countries.dart';
|
import 'package:aves/model/entry/cache.dart';
|
||||||
import 'package:aves/model/entry_cache.dart';
|
import 'package:aves/model/entry/dirs.dart';
|
||||||
import 'package:aves/model/entry_dirs.dart';
|
|
||||||
import 'package:aves/model/favourites.dart';
|
|
||||||
import 'package:aves/model/geotiff.dart';
|
|
||||||
import 'package:aves/model/metadata/address.dart';
|
import 'package:aves/model/metadata/address.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/model/metadata/trash.dart';
|
import 'package:aves/model/metadata/trash.dart';
|
||||||
import 'package:aves/model/multipage.dart';
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
|
||||||
import 'package:aves/model/source/trash.dart';
|
import 'package:aves/model/source/trash.dart';
|
||||||
import 'package:aves/model/video/metadata.dart';
|
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/service_policy.dart';
|
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/geocoding_service.dart';
|
|
||||||
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
import 'package:aves/utils/time_utils.dart';
|
import 'package:aves/utils/time_utils.dart';
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:country_code/country_code.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
|
|
||||||
enum EntryDataType { basic, aspectRatio, catalog, address, references }
|
enum EntryDataType { basic, aspectRatio, catalog, address, references }
|
||||||
|
|
||||||
class EntryOrigins {
|
class AvesEntry with AvesEntryBase {
|
||||||
static const int mediaStoreContent = 0;
|
@override
|
||||||
static const int unknownContent = 1;
|
|
||||||
static const int file = 2;
|
|
||||||
static const int vault = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AvesEntry {
|
|
||||||
// `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode
|
|
||||||
int id;
|
int id;
|
||||||
|
|
||||||
|
@override
|
||||||
String uri;
|
String uri;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int? pageId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int? sizeBytes;
|
||||||
|
|
||||||
String? _path, _filename, _extension, _sourceTitle;
|
String? _path, _filename, _extension, _sourceTitle;
|
||||||
EntryDir? _directory;
|
EntryDir? _directory;
|
||||||
int? pageId, contentId;
|
int? contentId;
|
||||||
final String sourceMimeType;
|
final String sourceMimeType;
|
||||||
int width, height, sourceRotationDegrees;
|
int width, height, sourceRotationDegrees;
|
||||||
int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
|
int? dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
|
||||||
bool trashed;
|
bool trashed;
|
||||||
int origin;
|
int origin;
|
||||||
|
|
||||||
|
@ -57,7 +48,11 @@ class AvesEntry {
|
||||||
|
|
||||||
List<AvesEntry>? burstEntries;
|
List<AvesEntry>? burstEntries;
|
||||||
|
|
||||||
final AChangeNotifier visualChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
@override
|
||||||
|
final AChangeNotifier visualChangeNotifier = AChangeNotifier();
|
||||||
|
|
||||||
|
final AChangeNotifier metadataChangeNotifier = AChangeNotifier();
|
||||||
|
final AChangeNotifier addressChangeNotifier = AChangeNotifier();
|
||||||
|
|
||||||
AvesEntry({
|
AvesEntry({
|
||||||
required int? id,
|
required int? id,
|
||||||
|
@ -243,140 +238,8 @@ class AvesEntry {
|
||||||
// so we use the one found during cataloguing if possible
|
// so we use the one found during cataloguing if possible
|
||||||
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
|
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
|
||||||
|
|
||||||
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
|
||||||
|
|
||||||
bool get isFavourite => favourites.isFavourite(this);
|
|
||||||
|
|
||||||
bool get isSvg => mimeType == MimeTypes.svg;
|
|
||||||
|
|
||||||
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
|
|
||||||
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
|
|
||||||
|
|
||||||
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
|
|
||||||
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
|
|
||||||
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
|
|
||||||
bool get _supportedByBitmapRegionDecoder =>
|
|
||||||
[
|
|
||||||
MimeTypes.heic,
|
|
||||||
MimeTypes.heif,
|
|
||||||
MimeTypes.jpeg,
|
|
||||||
MimeTypes.png,
|
|
||||||
MimeTypes.webp,
|
|
||||||
MimeTypes.arw,
|
|
||||||
MimeTypes.cr2,
|
|
||||||
MimeTypes.nef,
|
|
||||||
MimeTypes.nrw,
|
|
||||||
MimeTypes.orf,
|
|
||||||
MimeTypes.pef,
|
|
||||||
MimeTypes.raf,
|
|
||||||
MimeTypes.rw2,
|
|
||||||
MimeTypes.srw,
|
|
||||||
].contains(mimeType) &&
|
|
||||||
!isAnimated;
|
|
||||||
|
|
||||||
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
|
||||||
|
|
||||||
bool get useTiles => supportTiling && (width > 4096 || height > 4096);
|
|
||||||
|
|
||||||
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
|
||||||
|
|
||||||
bool get isImage => MimeTypes.isImage(mimeType);
|
|
||||||
|
|
||||||
bool get isVideo => MimeTypes.isVideo(mimeType);
|
|
||||||
|
|
||||||
bool get isCatalogued => _catalogMetadata != null;
|
bool get isCatalogued => _catalogMetadata != null;
|
||||||
|
|
||||||
bool get isAnimated => _catalogMetadata?.isAnimated ?? false;
|
|
||||||
|
|
||||||
bool get isGeotiff => _catalogMetadata?.isGeotiff ?? false;
|
|
||||||
|
|
||||||
bool get is360 => _catalogMetadata?.is360 ?? false;
|
|
||||||
|
|
||||||
bool get isMediaStoreContent => uri.startsWith('content://media/');
|
|
||||||
|
|
||||||
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
|
|
||||||
|
|
||||||
bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
|
|
||||||
|
|
||||||
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent);
|
|
||||||
|
|
||||||
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
|
||||||
|
|
||||||
bool get canEditLocation => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
|
|
||||||
|
|
||||||
bool get canEditTitleDescription => canEdit && canEditXmp;
|
|
||||||
|
|
||||||
bool get canEditRating => canEdit && canEditXmp;
|
|
||||||
|
|
||||||
bool get canEditTags => canEdit && canEditXmp;
|
|
||||||
|
|
||||||
bool get canRotate => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
|
|
||||||
|
|
||||||
bool get canFlip => canEdit && canEditExif;
|
|
||||||
|
|
||||||
bool get canEditExif => MimeTypes.canEditExif(mimeType);
|
|
||||||
|
|
||||||
bool get canEditIptc => MimeTypes.canEditIptc(mimeType);
|
|
||||||
|
|
||||||
bool get canEditXmp => MimeTypes.canEditXmp(mimeType);
|
|
||||||
|
|
||||||
bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType);
|
|
||||||
|
|
||||||
// Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata,
|
|
||||||
// so it should be registered as width=1920, height=1080, orientation=90,
|
|
||||||
// but is incorrectly registered as width=1080, height=1920, orientation=0.
|
|
||||||
// Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time.
|
|
||||||
// Comparing width and height can help with the portrait FHD video example,
|
|
||||||
// but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90
|
|
||||||
bool get isRotated => rotationDegrees % 180 == 90;
|
|
||||||
|
|
||||||
static const ratioSeparator = '\u2236';
|
|
||||||
static const resolutionSeparator = ' \u00D7 ';
|
|
||||||
|
|
||||||
bool get isSized => width > 0 && height > 0;
|
|
||||||
|
|
||||||
String get resolutionText {
|
|
||||||
final ws = width;
|
|
||||||
final hs = height;
|
|
||||||
return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs';
|
|
||||||
}
|
|
||||||
|
|
||||||
String get aspectRatioText {
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
final gcd = width.gcd(height);
|
|
||||||
final w = width ~/ gcd;
|
|
||||||
final h = height ~/ gcd;
|
|
||||||
return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h';
|
|
||||||
} else {
|
|
||||||
return '?$ratioSeparator?';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double get displayAspectRatio {
|
|
||||||
if (width == 0 || height == 0) return 1;
|
|
||||||
return isRotated ? height / width : width / height;
|
|
||||||
}
|
|
||||||
|
|
||||||
Size get displaySize {
|
|
||||||
final w = width.toDouble();
|
|
||||||
final h = height.toDouble();
|
|
||||||
return isRotated ? Size(h, w) : Size(w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
Size videoDisplaySize(double sar) {
|
|
||||||
final size = displaySize;
|
|
||||||
if (sar != 1) {
|
|
||||||
final dar = displayAspectRatio * sar;
|
|
||||||
final w = size.width;
|
|
||||||
final h = size.height;
|
|
||||||
if (w >= h) return Size(w, w / dar);
|
|
||||||
if (h > w) return Size(h * dar, h);
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
int get megaPixels => (width * height / 1000000).round();
|
|
||||||
|
|
||||||
DateTime? _bestDate;
|
DateTime? _bestDate;
|
||||||
|
|
||||||
DateTime? get bestDate {
|
DateTime? get bestDate {
|
||||||
|
@ -386,6 +249,7 @@ class AvesEntry {
|
||||||
|
|
||||||
int get rating => _catalogMetadata?.rating ?? 0;
|
int get rating => _catalogMetadata?.rating ?? 0;
|
||||||
|
|
||||||
|
@override
|
||||||
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
|
||||||
|
|
||||||
set rotationDegrees(int rotationDegrees) {
|
set rotationDegrees(int rotationDegrees) {
|
||||||
|
@ -397,6 +261,27 @@ class AvesEntry {
|
||||||
|
|
||||||
set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped;
|
set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped;
|
||||||
|
|
||||||
|
// Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata,
|
||||||
|
// so it should be registered as width=1920, height=1080, orientation=90,
|
||||||
|
// but is incorrectly registered as width=1080, height=1920, orientation=0.
|
||||||
|
// Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time.
|
||||||
|
// Comparing width and height can help with the portrait FHD video example,
|
||||||
|
// but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90
|
||||||
|
bool get isRotated => rotationDegrees % 180 == 90;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get displayAspectRatio {
|
||||||
|
if (width == 0 || height == 0) return 1;
|
||||||
|
return isRotated ? height / width : width / height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get displaySize {
|
||||||
|
final w = width.toDouble();
|
||||||
|
final h = height.toDouble();
|
||||||
|
return isRotated ? Size(h, w) : Size(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
String? get sourceTitle => _sourceTitle;
|
String? get sourceTitle => _sourceTitle;
|
||||||
|
|
||||||
set sourceTitle(String? sourceTitle) {
|
set sourceTitle(String? sourceTitle) {
|
||||||
|
@ -423,6 +308,7 @@ class AvesEntry {
|
||||||
return d == null ? null : DateTime(d.year, d.month, d.day);
|
return d == null ? null : DateTime(d.year, d.month, d.day);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
int? get durationMillis => _durationMillis;
|
int? get durationMillis => _durationMillis;
|
||||||
|
|
||||||
set durationMillis(int? durationMillis) {
|
set durationMillis(int? durationMillis) {
|
||||||
|
@ -459,8 +345,6 @@ class AvesEntry {
|
||||||
// derived from Google reverse geocoding addresses
|
// derived from Google reverse geocoding addresses
|
||||||
bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;
|
bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;
|
||||||
|
|
||||||
LatLng? get latLng => hasGps ? LatLng(_catalogMetadata!.latitude!, _catalogMetadata!.longitude!) : null;
|
|
||||||
|
|
||||||
Set<String>? _tags;
|
Set<String>? _tags;
|
||||||
|
|
||||||
Set<String> get tags {
|
Set<String> get tags {
|
||||||
|
@ -504,53 +388,6 @@ class AvesEntry {
|
||||||
addressDetails = null;
|
addressDetails = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> catalog({required bool background, required bool force, required bool persist}) async {
|
|
||||||
if (isCatalogued && !force) return;
|
|
||||||
if (isSvg) {
|
|
||||||
// vector image sizing is not essential, so we should not spend time for it during loading
|
|
||||||
// but it is useful anyway (for aspect ratios etc.) so we size them during cataloguing
|
|
||||||
final size = await SvgMetadataService.getSize(this);
|
|
||||||
if (size != null) {
|
|
||||||
final fields = {
|
|
||||||
'width': size.width.ceil(),
|
|
||||||
'height': size.height.ceil(),
|
|
||||||
};
|
|
||||||
await applyNewFields(fields, persist: persist);
|
|
||||||
}
|
|
||||||
catalogMetadata = CatalogMetadata(id: id);
|
|
||||||
} else {
|
|
||||||
// pre-processing
|
|
||||||
if (isVideo && (!isSized || durationMillis == 0)) {
|
|
||||||
// exotic video that is not sized during loading
|
|
||||||
final fields = await VideoMetadataFormatter.getLoadingMetadata(this);
|
|
||||||
await applyNewFields(fields, persist: persist);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cataloguing on platform
|
|
||||||
catalogMetadata = await metadataFetchService.getCatalogMetadata(this, background: background);
|
|
||||||
|
|
||||||
// post-processing
|
|
||||||
if (isVideo && (catalogMetadata?.dateMillis ?? 0) == 0) {
|
|
||||||
catalogMetadata = await VideoMetadataFormatter.getCatalogMetadata(this);
|
|
||||||
}
|
|
||||||
if (isGeotiff && !hasGps) {
|
|
||||||
final info = await metadataFetchService.getGeoTiffInfo(this);
|
|
||||||
if (info != null) {
|
|
||||||
final center = MappedGeoTiff(
|
|
||||||
info: info,
|
|
||||||
entry: this,
|
|
||||||
).center;
|
|
||||||
if (center != null) {
|
|
||||||
catalogMetadata = catalogMetadata?.copyWith(
|
|
||||||
latitude: center.latitude,
|
|
||||||
longitude: center.longitude,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddressDetails? get addressDetails => _addressDetails;
|
AddressDetails? get addressDetails => _addressDetails;
|
||||||
|
|
||||||
set addressDetails(AddressDetails? newAddress) {
|
set addressDetails(AddressDetails? newAddress) {
|
||||||
|
@ -558,79 +395,6 @@ class AvesEntry {
|
||||||
addressChangeNotifier.notify();
|
addressChangeNotifier.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
|
|
||||||
if (hasGps) {
|
|
||||||
await _locateCountry(force: force);
|
|
||||||
if (await availability.canLocatePlaces) {
|
|
||||||
await locatePlace(background: background, force: force, geocoderLocale: geocoderLocale);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addressDetails = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// quick reverse geocoding to find the country, using an offline asset
|
|
||||||
Future<void> _locateCountry({required bool force}) async {
|
|
||||||
if (!hasGps || (hasAddress && !force)) return;
|
|
||||||
final countryCode = await countryTopology.countryCode(latLng!);
|
|
||||||
setCountry(countryCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCountry(CountryCode? countryCode) {
|
|
||||||
if (hasFineAddress || countryCode == null) return;
|
|
||||||
addressDetails = AddressDetails(
|
|
||||||
id: id,
|
|
||||||
countryCode: countryCode.alpha2,
|
|
||||||
countryName: countryCode.alpha3,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// full reverse geocoding, requiring Play Services and some connectivity
|
|
||||||
Future<void> locatePlace({required bool background, required bool force, required Locale geocoderLocale}) async {
|
|
||||||
if (!hasGps || (hasFineAddress && !force)) return;
|
|
||||||
try {
|
|
||||||
Future<List<Address>> call() => GeocodingService.getAddress(latLng!, geocoderLocale);
|
|
||||||
final addresses = await (background
|
|
||||||
? servicePolicy.call(
|
|
||||||
call,
|
|
||||||
priority: ServiceCallPriority.getLocation,
|
|
||||||
)
|
|
||||||
: call());
|
|
||||||
if (addresses.isNotEmpty) {
|
|
||||||
final address = addresses.first;
|
|
||||||
final cc = address.countryCode?.toUpperCase();
|
|
||||||
final cn = address.countryName;
|
|
||||||
final aa = address.adminArea;
|
|
||||||
addressDetails = AddressDetails(
|
|
||||||
id: id,
|
|
||||||
countryCode: cc,
|
|
||||||
countryName: cn,
|
|
||||||
adminArea: aa,
|
|
||||||
// if country & admin fields are null, it is likely the ocean,
|
|
||||||
// which is identified by `featureName` but we default to the address line anyway
|
|
||||||
locality: address.locality ?? (cc == null && cn == null && aa == null ? address.addressLine : null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error, stack) {
|
|
||||||
debugPrint('$runtimeType locate failed with path=$path coordinates=$latLng error=$error\n$stack');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> findAddressLine({required Locale geocoderLocale}) async {
|
|
||||||
if (!hasGps) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final addresses = await GeocodingService.getAddress(latLng!, geocoderLocale);
|
|
||||||
if (addresses.isNotEmpty) {
|
|
||||||
final address = addresses.first;
|
|
||||||
return address.addressLine;
|
|
||||||
}
|
|
||||||
} catch (error, stack) {
|
|
||||||
debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$latLng error=$error\n$stack');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String get shortAddress {
|
String get shortAddress {
|
||||||
// `admin area` examples: Seoul, Geneva, null
|
// `admin area` examples: Seoul, Geneva, null
|
||||||
// `locality` examples: Mapo-gu, Geneva, Annecy
|
// `locality` examples: Mapo-gu, Geneva, Annecy
|
||||||
|
@ -732,107 +496,4 @@ class AvesEntry {
|
||||||
visualChangeNotifier.notify();
|
visualChangeNotifier.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// favourites
|
|
||||||
|
|
||||||
Future<void> toggleFavourite() async {
|
|
||||||
if (isFavourite) {
|
|
||||||
await removeFromFavourites();
|
|
||||||
} else {
|
|
||||||
await addToFavourites();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addToFavourites() async {
|
|
||||||
if (!isFavourite) {
|
|
||||||
await favourites.add({this});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeFromFavourites() async {
|
|
||||||
if (isFavourite) {
|
|
||||||
await favourites.removeEntries({this});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// multipage
|
|
||||||
|
|
||||||
static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
|
|
||||||
|
|
||||||
bool get isMultiPage => (_catalogMetadata?.isMultiPage ?? false) || isBurst;
|
|
||||||
|
|
||||||
bool get isBurst => burstEntries?.isNotEmpty == true;
|
|
||||||
|
|
||||||
// for backward compatibility
|
|
||||||
bool get _isMotionPhotoLegacy => isMultiPage && !isBurst && mimeType == MimeTypes.jpeg;
|
|
||||||
|
|
||||||
bool get isMotionPhoto => (_catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
|
|
||||||
|
|
||||||
String? get burstKey {
|
|
||||||
if (filenameWithoutExtension != null) {
|
|
||||||
final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
|
|
||||||
if (match != null) {
|
|
||||||
return '$directory/${match.group(1)}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<MultiPageInfo?> getMultiPageInfo() async {
|
|
||||||
if (isBurst) {
|
|
||||||
return MultiPageInfo(
|
|
||||||
mainEntry: this,
|
|
||||||
pages: burstEntries!
|
|
||||||
.mapIndexed((index, entry) => SinglePageInfo(
|
|
||||||
index: index,
|
|
||||||
pageId: entry.id,
|
|
||||||
isDefault: index == 0,
|
|
||||||
uri: entry.uri,
|
|
||||||
mimeType: entry.mimeType,
|
|
||||||
width: entry.width,
|
|
||||||
height: entry.height,
|
|
||||||
rotationDegrees: entry.rotationDegrees,
|
|
||||||
durationMillis: entry.durationMillis,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return await metadataFetchService.getMultiPageInfo(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort
|
|
||||||
|
|
||||||
// compare by:
|
|
||||||
// 1) title ascending
|
|
||||||
// 2) extension ascending
|
|
||||||
static int compareByName(AvesEntry a, AvesEntry b) {
|
|
||||||
final c = compareAsciiUpperCaseNatural(a.bestTitle ?? '', b.bestTitle ?? '');
|
|
||||||
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare by:
|
|
||||||
// 1) date descending
|
|
||||||
// 2) name descending
|
|
||||||
static int compareByDate(AvesEntry a, AvesEntry b) {
|
|
||||||
var c = (b.bestDate ?? epoch).compareTo(a.bestDate ?? epoch);
|
|
||||||
if (c != 0) return c;
|
|
||||||
return compareByName(b, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare by:
|
|
||||||
// 1) rating descending
|
|
||||||
// 2) date descending
|
|
||||||
static int compareByRating(AvesEntry a, AvesEntry b) {
|
|
||||||
final c = b.rating.compareTo(a.rating);
|
|
||||||
return c != 0 ? c : compareByDate(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare by:
|
|
||||||
// 1) size descending
|
|
||||||
// 2) date descending
|
|
||||||
static int compareBySize(AvesEntry a, AvesEntry b) {
|
|
||||||
final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0);
|
|
||||||
return c != 0 ? c : compareByDate(a, b);
|
|
||||||
}
|
|
||||||
}
|
}
|
57
lib/model/entry/extensions/catalog.dart
Normal file
57
lib/model/entry/extensions/catalog.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
|
import 'package:aves/model/geotiff.dart';
|
||||||
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
|
import 'package:aves/model/video/metadata.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
||||||
|
|
||||||
|
// TODO TLAD [split] need props
|
||||||
|
extension ExtraAvesEntryCatalog on AvesEntry {
|
||||||
|
Future<void> catalog({required bool background, required bool force, required bool persist}) async {
|
||||||
|
if (isCatalogued && !force) return;
|
||||||
|
if (isSvg) {
|
||||||
|
// vector image sizing is not essential, so we should not spend time for it during loading
|
||||||
|
// but it is useful anyway (for aspect ratios etc.) so we size them during cataloguing
|
||||||
|
final size = await SvgMetadataService.getSize(this);
|
||||||
|
if (size != null) {
|
||||||
|
final fields = {
|
||||||
|
'width': size.width.ceil(),
|
||||||
|
'height': size.height.ceil(),
|
||||||
|
};
|
||||||
|
await applyNewFields(fields, persist: persist);
|
||||||
|
}
|
||||||
|
catalogMetadata = CatalogMetadata(id: id);
|
||||||
|
} else {
|
||||||
|
// pre-processing
|
||||||
|
if (isVideo && (!isSized || durationMillis == 0)) {
|
||||||
|
// exotic video that is not sized during loading
|
||||||
|
final fields = await VideoMetadataFormatter.getLoadingMetadata(this);
|
||||||
|
await applyNewFields(fields, persist: persist);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cataloguing on platform
|
||||||
|
catalogMetadata = await metadataFetchService.getCatalogMetadata(this, background: background);
|
||||||
|
|
||||||
|
// post-processing
|
||||||
|
if (isVideo && (catalogMetadata?.dateMillis ?? 0) == 0) {
|
||||||
|
catalogMetadata = await VideoMetadataFormatter.getCatalogMetadata(this);
|
||||||
|
}
|
||||||
|
if (isGeotiff && !hasGps) {
|
||||||
|
final info = await metadataFetchService.getGeoTiffInfo(this);
|
||||||
|
if (info != null) {
|
||||||
|
final center = MappedGeoTiff(
|
||||||
|
info: info,
|
||||||
|
entry: this,
|
||||||
|
).center;
|
||||||
|
if (center != null) {
|
||||||
|
catalogMetadata = catalogMetadata?.copyWith(
|
||||||
|
latitude: center.latitude,
|
||||||
|
longitude: center.longitude,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
lib/model/entry/extensions/favourites.dart
Normal file
26
lib/model/entry/extensions/favourites.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/favourites.dart';
|
||||||
|
|
||||||
|
extension ExtraAvesEntryFav on AvesEntry {
|
||||||
|
bool get isFavourite => favourites.isFavourite(this);
|
||||||
|
|
||||||
|
Future<void> toggleFavourite() async {
|
||||||
|
if (isFavourite) {
|
||||||
|
await removeFromFavourites();
|
||||||
|
} else {
|
||||||
|
await addToFavourites();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addToFavourites() async {
|
||||||
|
if (!isFavourite) {
|
||||||
|
await favourites.add({this});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeFromFavourites() async {
|
||||||
|
if (isFavourite) {
|
||||||
|
await favourites.removeEntries({this});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
lib/model/entry/extensions/images.dart
Normal file
79
lib/model/entry/extensions/images.dart
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/image_providers/region_provider.dart';
|
||||||
|
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||||
|
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||||
|
import 'package:aves/model/entry/cache.dart';
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/utils/math_utils.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
extension ExtraAvesEntryImages on AvesEntry {
|
||||||
|
bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent));
|
||||||
|
|
||||||
|
ThumbnailProvider getThumbnail({double extent = 0}) {
|
||||||
|
return ThumbnailProvider(_getThumbnailProviderKey(extent));
|
||||||
|
}
|
||||||
|
|
||||||
|
ThumbnailProviderKey _getThumbnailProviderKey(double extent) {
|
||||||
|
EntryCache.markThumbnailExtent(extent);
|
||||||
|
return ThumbnailProviderKey(
|
||||||
|
uri: uri,
|
||||||
|
mimeType: mimeType,
|
||||||
|
pageId: pageId,
|
||||||
|
rotationDegrees: rotationDegrees,
|
||||||
|
isFlipped: isFlipped,
|
||||||
|
dateModifiedSecs: dateModifiedSecs ?? -1,
|
||||||
|
extent: extent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionProvider getRegion({int sampleSize = 1, double scale = 1, required Rectangle<num> region}) {
|
||||||
|
return RegionProvider(RegionProviderKey(
|
||||||
|
uri: uri,
|
||||||
|
mimeType: mimeType,
|
||||||
|
pageId: pageId,
|
||||||
|
sizeBytes: sizeBytes,
|
||||||
|
rotationDegrees: rotationDegrees,
|
||||||
|
isFlipped: isFlipped,
|
||||||
|
sampleSize: sampleSize,
|
||||||
|
region: Rectangle(
|
||||||
|
(region.left * scale).round(),
|
||||||
|
(region.top * scale).round(),
|
||||||
|
(region.width * scale).round(),
|
||||||
|
(region.height * scale).round(),
|
||||||
|
),
|
||||||
|
imageSize: Size((width * scale).toDouble(), (height * scale).toDouble()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
UriImage get uriImage => UriImage(
|
||||||
|
uri: uri,
|
||||||
|
mimeType: mimeType,
|
||||||
|
pageId: pageId,
|
||||||
|
rotationDegrees: rotationDegrees,
|
||||||
|
isFlipped: isFlipped,
|
||||||
|
sizeBytes: sizeBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive;
|
||||||
|
|
||||||
|
List<ThumbnailProvider> get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList();
|
||||||
|
|
||||||
|
ThumbnailProvider get bestCachedThumbnail {
|
||||||
|
final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady);
|
||||||
|
return sizedThumbnailKey != null ? ThumbnailProvider(sizedThumbnailKey) : getThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// magic number used to derive sample size from scale
|
||||||
|
static const scaleFactor = 2.0;
|
||||||
|
|
||||||
|
static int sampleSizeForScale(double scale) {
|
||||||
|
var sample = 0;
|
||||||
|
if (0 < scale && scale < 1) {
|
||||||
|
sample = highestPowerOf2((1 / scale) / scaleFactor);
|
||||||
|
}
|
||||||
|
return max<int>(1, sample);
|
||||||
|
}
|
||||||
|
}
|
161
lib/model/entry/extensions/info.dart
Normal file
161
lib/model/entry/extensions/info.dart
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
|
import 'package:aves/model/video/keys.dart';
|
||||||
|
import 'package:aves/model/video/metadata.dart';
|
||||||
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
||||||
|
import 'package:aves/theme/colors.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
// TODO TLAD [split] need props/multipage
|
||||||
|
extension ExtraAvesEntryInfo on AvesEntry {
|
||||||
|
// directory names may contain the name of their parent directory (as prefix + '/')
|
||||||
|
// directory names may contain an index (as suffix in '[]')
|
||||||
|
static final directoryNamePattern = RegExp(r'^((?<parent>.*?)/)?(?<name>.*?)(\[(?<index>\d+)\])?$');
|
||||||
|
|
||||||
|
Future<List<MapEntry<String, MetadataDirectory>>> getMetadataDirectories(BuildContext context) async {
|
||||||
|
final rawMetadata = await (isSvg ? SvgMetadataService.getAllMetadata(this) : metadataFetchService.getAllMetadata(this));
|
||||||
|
final directories = rawMetadata.entries.map((dirKV) {
|
||||||
|
var directoryName = dirKV.key as String;
|
||||||
|
|
||||||
|
String? parent;
|
||||||
|
int? index;
|
||||||
|
final match = directoryNamePattern.firstMatch(directoryName);
|
||||||
|
if (match != null) {
|
||||||
|
parent = match.namedGroup('parent');
|
||||||
|
final nameMatch = match.namedGroup('name');
|
||||||
|
if (nameMatch != null) {
|
||||||
|
directoryName = nameMatch;
|
||||||
|
}
|
||||||
|
final indexMatch = match.namedGroup('index');
|
||||||
|
if (indexMatch != null) {
|
||||||
|
index = int.tryParse(indexMatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final rawTags = dirKV.value as Map;
|
||||||
|
return MetadataDirectory(
|
||||||
|
directoryName,
|
||||||
|
_toSortedTags(rawTags),
|
||||||
|
parent: parent,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (isVideo || (mimeType == MimeTypes.heif && isMultiPage)) {
|
||||||
|
directories.addAll(await _getStreamDirectories(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
final titledDirectories = directories.map((dir) {
|
||||||
|
var title = dir.name;
|
||||||
|
if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) {
|
||||||
|
title = '${dir.parent}/$title';
|
||||||
|
}
|
||||||
|
if (dir.index != null) {
|
||||||
|
title += ' ${dir.index}';
|
||||||
|
}
|
||||||
|
return MapEntry(title, dir);
|
||||||
|
}).toList()
|
||||||
|
..sort((a, b) => compareAsciiUpperCase(a.key, b.key));
|
||||||
|
|
||||||
|
return titledDirectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<MetadataDirectory>> _getStreamDirectories(BuildContext context) async {
|
||||||
|
final directories = <MetadataDirectory>[];
|
||||||
|
final mediaInfo = await VideoMetadataFormatter.getVideoMetadata(this);
|
||||||
|
|
||||||
|
final formattedMediaTags = VideoMetadataFormatter.formatInfo(mediaInfo);
|
||||||
|
if (formattedMediaTags.isNotEmpty) {
|
||||||
|
// overwrite generic directory found from the platform side
|
||||||
|
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, _toSortedTags(formattedMediaTags)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaInfo.containsKey(Keys.streams)) {
|
||||||
|
String getTypeText(Map stream) {
|
||||||
|
final type = stream[Keys.streamType] ?? StreamTypes.unknown;
|
||||||
|
switch (type) {
|
||||||
|
case StreamTypes.attachment:
|
||||||
|
return 'Attachment';
|
||||||
|
case StreamTypes.audio:
|
||||||
|
return 'Audio';
|
||||||
|
case StreamTypes.metadata:
|
||||||
|
return 'Metadata';
|
||||||
|
case StreamTypes.subtitle:
|
||||||
|
case StreamTypes.timedText:
|
||||||
|
return 'Text';
|
||||||
|
case StreamTypes.video:
|
||||||
|
return stream.containsKey(Keys.fpsDen) ? 'Video' : 'Image';
|
||||||
|
case StreamTypes.unknown:
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final allStreams = (mediaInfo[Keys.streams] as List).cast<Map>();
|
||||||
|
final attachmentStreams = allStreams.where((stream) => stream[Keys.streamType] == StreamTypes.attachment).toList();
|
||||||
|
final knownStreams = allStreams.whereNot(attachmentStreams.contains);
|
||||||
|
|
||||||
|
// display known streams as separate directories (e.g. video, audio, subs)
|
||||||
|
if (knownStreams.isNotEmpty) {
|
||||||
|
final indexDigits = knownStreams.length.toString().length;
|
||||||
|
|
||||||
|
final colors = context.read<AvesColorsData>();
|
||||||
|
for (final stream in knownStreams) {
|
||||||
|
final index = (stream[Keys.index] ?? 0) + 1;
|
||||||
|
final typeText = getTypeText(stream);
|
||||||
|
final dirName = [
|
||||||
|
'Stream ${index.toString().padLeft(indexDigits, '0')}',
|
||||||
|
typeText,
|
||||||
|
].join(Constants.separator);
|
||||||
|
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
|
||||||
|
if (formattedStreamTags.isNotEmpty) {
|
||||||
|
final color = colors.fromString(typeText);
|
||||||
|
directories.add(MetadataDirectory(dirName, _toSortedTags(formattedStreamTags), color: color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// group attachments by format (e.g. TTF fonts)
|
||||||
|
if (attachmentStreams.isNotEmpty) {
|
||||||
|
final formatCount = <String, List<String?>>{};
|
||||||
|
for (final stream in attachmentStreams) {
|
||||||
|
final codec = (stream[Keys.codecName] as String? ?? 'unknown').toUpperCase();
|
||||||
|
if (!formatCount.containsKey(codec)) {
|
||||||
|
formatCount[codec] = [];
|
||||||
|
}
|
||||||
|
formatCount[codec]!.add(stream[Keys.filename]);
|
||||||
|
}
|
||||||
|
if (formatCount.isNotEmpty) {
|
||||||
|
final rawTags = formatCount.map((key, value) {
|
||||||
|
final count = value.length;
|
||||||
|
// remove duplicate names, so number of displayed names may not match displayed count
|
||||||
|
final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase);
|
||||||
|
return MapEntry(key, '$count items: ${names.join(', ')}');
|
||||||
|
});
|
||||||
|
directories.add(MetadataDirectory('Attachments', _toSortedTags(rawTags)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplayTreeMap<String, String> _toSortedTags(Map rawTags) {
|
||||||
|
final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) {
|
||||||
|
var value = (tagKV.value as String? ?? '').trim();
|
||||||
|
if (value.isEmpty) return null;
|
||||||
|
final tagName = tagKV.key as String;
|
||||||
|
return MapEntry(tagName, value);
|
||||||
|
}).whereNotNull()));
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
}
|
89
lib/model/entry/extensions/location.dart
Normal file
89
lib/model/entry/extensions/location.dart
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:aves/geo/countries.dart';
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/metadata/address.dart';
|
||||||
|
import 'package:aves/services/common/service_policy.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/services/geocoding_service.dart';
|
||||||
|
import 'package:country_code/country_code.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
extension ExtraAvesEntryLocation on AvesEntry {
|
||||||
|
LatLng? get latLng => hasGps ? LatLng(catalogMetadata!.latitude!, catalogMetadata!.longitude!) : null;
|
||||||
|
|
||||||
|
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
|
||||||
|
if (hasGps) {
|
||||||
|
await _locateCountry(force: force);
|
||||||
|
if (await availability.canLocatePlaces) {
|
||||||
|
await locatePlace(background: background, force: force, geocoderLocale: geocoderLocale);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addressDetails = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quick reverse geocoding to find the country, using an offline asset
|
||||||
|
Future<void> _locateCountry({required bool force}) async {
|
||||||
|
if (!hasGps || (hasAddress && !force)) return;
|
||||||
|
final countryCode = await countryTopology.countryCode(latLng!);
|
||||||
|
setCountry(countryCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCountry(CountryCode? countryCode) {
|
||||||
|
if (hasFineAddress || countryCode == null) return;
|
||||||
|
addressDetails = AddressDetails(
|
||||||
|
id: id,
|
||||||
|
countryCode: countryCode.alpha2,
|
||||||
|
countryName: countryCode.alpha3,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// full reverse geocoding, requiring Play Services and some connectivity
|
||||||
|
Future<void> locatePlace({required bool background, required bool force, required Locale geocoderLocale}) async {
|
||||||
|
if (!hasGps || (hasFineAddress && !force)) return;
|
||||||
|
try {
|
||||||
|
Future<List<Address>> call() => GeocodingService.getAddress(latLng!, geocoderLocale);
|
||||||
|
final addresses = await (background
|
||||||
|
? servicePolicy.call(
|
||||||
|
call,
|
||||||
|
priority: ServiceCallPriority.getLocation,
|
||||||
|
)
|
||||||
|
: call());
|
||||||
|
if (addresses.isNotEmpty) {
|
||||||
|
final address = addresses.first;
|
||||||
|
final cc = address.countryCode?.toUpperCase();
|
||||||
|
final cn = address.countryName;
|
||||||
|
final aa = address.adminArea;
|
||||||
|
addressDetails = AddressDetails(
|
||||||
|
id: id,
|
||||||
|
countryCode: cc,
|
||||||
|
countryName: cn,
|
||||||
|
adminArea: aa,
|
||||||
|
// if country & admin fields are null, it is likely the ocean,
|
||||||
|
// which is identified by `featureName` but we default to the address line anyway
|
||||||
|
locality: address.locality ?? (cc == null && cn == null && aa == null ? address.addressLine : null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error, stack) {
|
||||||
|
debugPrint('$runtimeType locate failed with path=$path coordinates=$latLng error=$error\n$stack');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> findAddressLine({required Locale geocoderLocale}) async {
|
||||||
|
if (!hasGps) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final addresses = await GeocodingService.getAddress(latLng!, geocoderLocale);
|
||||||
|
if (addresses.isNotEmpty) {
|
||||||
|
final address = addresses.first;
|
||||||
|
return address.addressLine;
|
||||||
|
}
|
||||||
|
} catch (error, stack) {
|
||||||
|
debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$latLng error=$error\n$stack');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
590
lib/model/entry/extensions/metadata_edition.dart
Normal file
590
lib/model/entry/extensions/metadata_edition.dart
Normal file
|
@ -0,0 +1,590 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
|
import 'package:aves/model/metadata/enums/date_field_source.dart';
|
||||||
|
import 'package:aves/model/metadata/enums/enums.dart';
|
||||||
|
import 'package:aves/model/metadata/fields.dart';
|
||||||
|
import 'package:aves/ref/exif.dart';
|
||||||
|
import 'package:aves/ref/iptc.dart';
|
||||||
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/services/metadata/xmp.dart';
|
||||||
|
import 'package:aves/utils/time_utils.dart';
|
||||||
|
import 'package:aves/utils/xmp_utils.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
|
// TODO TLAD [split] need props/catalog
|
||||||
|
extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
|
Future<Set<EntryDataType>> editDate(DateModifier userModifier) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
|
||||||
|
final appliedModifier = await _applyDateModifierToEntry(userModifier);
|
||||||
|
if (appliedModifier == null) {
|
||||||
|
if (!isMissingAtPath && userModifier.action != DateEditAction.copyField) {
|
||||||
|
await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditExif && appliedModifier.fields.any((v) => v.type == MetadataType.exif)) {
|
||||||
|
final newFields = await metadataEditService.editExifDate(this, appliedModifier);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditXmp && appliedModifier.fields.any((v) => v.type == MetadataType.xmp)) {
|
||||||
|
final metadata = {
|
||||||
|
MetadataType.xmp: await _editXmp((descriptions) {
|
||||||
|
switch (appliedModifier.action) {
|
||||||
|
case DateEditAction.setCustom:
|
||||||
|
case DateEditAction.copyField:
|
||||||
|
case DateEditAction.copyItem:
|
||||||
|
case DateEditAction.extractFromTitle:
|
||||||
|
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
|
||||||
|
break;
|
||||||
|
case DateEditAction.shift:
|
||||||
|
final xmpDate = XMP.getString(descriptions, XMP.xmpCreateDate, namespace: Namespaces.xmp);
|
||||||
|
if (xmpDate != null) {
|
||||||
|
final date = DateTime.tryParse(xmpDate);
|
||||||
|
if (date != null) {
|
||||||
|
// TODO TLAD [date] DateTime.tryParse converts to UTC time, losing the time zone offset
|
||||||
|
final shiftedDate = date.add(Duration(minutes: appliedModifier.shiftMinutes!));
|
||||||
|
editCreateDateXmp(descriptions, shiftedDate);
|
||||||
|
} else {
|
||||||
|
reportService.recordError('failed to parse XMP date=$xmpDate', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DateEditAction.remove:
|
||||||
|
editCreateDateXmp(descriptions, null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final removalLocation = LatLng(0, 0);
|
||||||
|
|
||||||
|
Future<Set<EntryDataType>> editLocation(LatLng? latLng) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
final metadata = <MetadataType, dynamic>{};
|
||||||
|
|
||||||
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
|
|
||||||
|
if (canEditExif) {
|
||||||
|
// clear every GPS field
|
||||||
|
final exifFields = Map<MetadataField, dynamic>.fromEntries(MetadataFields.exifGpsFields.map((k) => MapEntry(k, null)));
|
||||||
|
// add latitude & longitude, if any
|
||||||
|
if (latLng != null && latLng != removalLocation) {
|
||||||
|
final latitude = latLng.latitude;
|
||||||
|
final longitude = latLng.longitude;
|
||||||
|
exifFields.addAll({
|
||||||
|
MetadataField.exifGpsLatitude: latitude.abs(),
|
||||||
|
MetadataField.exifGpsLatitudeRef: latitude >= 0 ? Exif.latitudeNorth : Exif.latitudeSouth,
|
||||||
|
MetadataField.exifGpsLongitude: longitude.abs(),
|
||||||
|
MetadataField.exifGpsLongitudeRef: longitude >= 0 ? Exif.longitudeEast : Exif.longitudeWest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
metadata[MetadataType.exif] = Map<String, dynamic>.fromEntries(exifFields.entries.map((kv) => MapEntry(kv.key.toPlatform!, kv.value)));
|
||||||
|
|
||||||
|
if (canEditXmp && missingDate != null) {
|
||||||
|
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||||
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mimeType == MimeTypes.mp4) {
|
||||||
|
final mp4Fields = <MetadataField, String?>{};
|
||||||
|
|
||||||
|
String? iso6709String;
|
||||||
|
if (latLng != null && latLng != removalLocation) {
|
||||||
|
final latitude = latLng.latitude;
|
||||||
|
final longitude = latLng.longitude;
|
||||||
|
const locale = 'en_US';
|
||||||
|
final isoLat = '${latitude >= 0 ? '+' : '-'}${NumberFormat('00.0000', locale).format(latitude.abs())}';
|
||||||
|
final isoLon = '${longitude >= 0 ? '+' : '-'}${NumberFormat('000.0000', locale).format(longitude.abs())}';
|
||||||
|
iso6709String = '$isoLat$isoLon/';
|
||||||
|
}
|
||||||
|
mp4Fields[MetadataField.mp4GpsCoordinates] = iso6709String;
|
||||||
|
|
||||||
|
if (missingDate != null) {
|
||||||
|
final xmpParts = await _editXmp((descriptions) {
|
||||||
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
mp4Fields[MetadataField.mp4Xmp] = xmpParts[xmpCoreKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata[MetadataType.mp4] = Map<String, String?>.fromEntries(mp4Fields.entries.map((kv) => MapEntry(kv.key.toPlatform!, kv.value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.catalog,
|
||||||
|
EntryDataType.address,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<EntryDataType>> _changeExifOrientation(Future<Map<String, dynamic>> Function() apply) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
|
||||||
|
await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
|
|
||||||
|
final newFields = await apply();
|
||||||
|
// applying fields is only useful for a smoother visual change,
|
||||||
|
// as proper refreshing and persistence happens at the caller level
|
||||||
|
await applyNewFields(newFields, persist: false);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.aspectRatio,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<EntryDataType>> _rotateMp4(int rotationDegrees) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
|
||||||
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
|
|
||||||
|
final mp4Fields = <MetadataField, String?>{
|
||||||
|
MetadataField.mp4RotationDegrees: rotationDegrees.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (missingDate != null) {
|
||||||
|
final xmpParts = await _editXmp((descriptions) {
|
||||||
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
mp4Fields[MetadataField.mp4Xmp] = xmpParts[xmpCoreKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadata = <MetadataType, dynamic>{
|
||||||
|
MetadataType.mp4: Map<String, String?>.fromEntries(mp4Fields.entries.map((kv) => MapEntry(kv.key.toPlatform!, kv.value))),
|
||||||
|
};
|
||||||
|
|
||||||
|
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||||
|
// applying fields is only useful for a smoother visual change,
|
||||||
|
// as proper refreshing and persistence happens at the caller level
|
||||||
|
await applyNewFields(newFields, persist: false);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.aspectRatio,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<EntryDataType>> rotate({required bool clockwise}) {
|
||||||
|
if (mimeType == MimeTypes.mp4) {
|
||||||
|
return _rotateMp4((rotationDegrees + (clockwise ? 90 : -90) + 360) % 360);
|
||||||
|
} else {
|
||||||
|
return _changeExifOrientation(() => metadataEditService.rotate(this, clockwise: clockwise));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<EntryDataType>> flip() {
|
||||||
|
return _changeExifOrientation(() => metadataEditService.flip(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// write title:
|
||||||
|
// - IPTC / object-name, if IPTC exists
|
||||||
|
// - XMP / dc:title
|
||||||
|
// write description:
|
||||||
|
// - Exif / ImageDescription
|
||||||
|
// - IPTC / caption-abstract, if IPTC exists
|
||||||
|
// - XMP / dc:description
|
||||||
|
Future<Set<EntryDataType>> editTitleDescription(Map<DescriptionField, String?> fields) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
final metadata = <MetadataType, dynamic>{};
|
||||||
|
|
||||||
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
|
|
||||||
|
final editTitle = fields.keys.contains(DescriptionField.title);
|
||||||
|
final editDescription = fields.keys.contains(DescriptionField.description);
|
||||||
|
final title = fields[DescriptionField.title];
|
||||||
|
final description = fields[DescriptionField.description];
|
||||||
|
|
||||||
|
if (canEditExif && editDescription) {
|
||||||
|
metadata[MetadataType.exif] = {
|
||||||
|
MetadataField.exifImageDescription.toPlatform!: null,
|
||||||
|
MetadataField.exifUserComment.toPlatform!: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditIptc) {
|
||||||
|
final iptc = await metadataFetchService.getIptc(this);
|
||||||
|
if (iptc != null) {
|
||||||
|
if (editTitle) {
|
||||||
|
editIptcValues(iptc, IPTC.applicationRecord, IPTC.objectName, {if (title != null) title});
|
||||||
|
}
|
||||||
|
if (editDescription) {
|
||||||
|
editIptcValues(iptc, IPTC.applicationRecord, IPTC.captionAbstractTag, {if (description != null) description});
|
||||||
|
}
|
||||||
|
metadata[MetadataType.iptc] = iptc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditXmp) {
|
||||||
|
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||||
|
var modified = false;
|
||||||
|
if (editTitle) {
|
||||||
|
modified |= XMP.setAttribute(
|
||||||
|
descriptions,
|
||||||
|
XMP.dcTitle,
|
||||||
|
title,
|
||||||
|
namespace: Namespaces.dc,
|
||||||
|
strat: XmpEditStrategy.always,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (editDescription) {
|
||||||
|
modified |= XMP.setAttribute(
|
||||||
|
descriptions,
|
||||||
|
XMP.dcDescription,
|
||||||
|
description,
|
||||||
|
namespace: Namespaces.dc,
|
||||||
|
strat: XmpEditStrategy.always,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (modified && missingDate != null) {
|
||||||
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write:
|
||||||
|
// - IPTC / keywords, if IPTC exists
|
||||||
|
// - XMP / dc:subject
|
||||||
|
Future<Set<EntryDataType>> editTags(Set<String> tags) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
final metadata = <MetadataType, dynamic>{};
|
||||||
|
|
||||||
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
|
|
||||||
|
if (canEditIptc) {
|
||||||
|
final iptc = await metadataFetchService.getIptc(this);
|
||||||
|
if (iptc != null) {
|
||||||
|
editIptcValues(iptc, IPTC.applicationRecord, IPTC.keywordsTag, tags);
|
||||||
|
metadata[MetadataType.iptc] = iptc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditXmp) {
|
||||||
|
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||||
|
final modified = editTagsXmp(descriptions, tags);
|
||||||
|
if (modified && missingDate != null) {
|
||||||
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.add(EntryDataType.catalog);
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write:
|
||||||
|
// - XMP / xmp:Rating
|
||||||
|
// update:
|
||||||
|
// - XMP / MicrosoftPhoto:Rating
|
||||||
|
// ignore (Windows tags, not part of Exif 2.32 spec):
|
||||||
|
// - Exif / Rating
|
||||||
|
// - Exif / RatingPercent
|
||||||
|
Future<Set<EntryDataType>> editRating(int? rating) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
final metadata = <MetadataType, dynamic>{};
|
||||||
|
|
||||||
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
|
|
||||||
|
if (canEditXmp) {
|
||||||
|
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||||
|
final modified = editRatingXmp(descriptions, rating);
|
||||||
|
if (modified && missingDate != null) {
|
||||||
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final newFields = await metadataEditService.editMetadata(this, metadata);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.add(EntryDataType.catalog);
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove:
|
||||||
|
// - trailer video
|
||||||
|
// - XMP / Container:Directory
|
||||||
|
// - XMP / GCamera:MicroVideo*
|
||||||
|
// - XMP / GCamera:MotionPhoto*
|
||||||
|
Future<Set<EntryDataType>> removeTrailerVideo() async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
final metadata = <MetadataType, dynamic>{};
|
||||||
|
|
||||||
|
if (!canEditXmp) return dataTypes;
|
||||||
|
|
||||||
|
final missingDate = await _missingDateCheckAndExifEdit(dataTypes);
|
||||||
|
|
||||||
|
final newFields = await metadataEditService.removeTrailerVideo(this);
|
||||||
|
|
||||||
|
metadata[MetadataType.xmp] = await _editXmp((descriptions) {
|
||||||
|
final modified = removeContainerXmp(descriptions);
|
||||||
|
if (modified && missingDate != null) {
|
||||||
|
editCreateDateXmp(descriptions, missingDate);
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
});
|
||||||
|
|
||||||
|
newFields.addAll(await metadataEditService.editMetadata(this, metadata, autoCorrectTrailerOffset: false));
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.add(EntryDataType.catalog);
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<EntryDataType>> removeMetadata(Set<MetadataType> types) async {
|
||||||
|
final dataTypes = <EntryDataType>{};
|
||||||
|
|
||||||
|
final newFields = await metadataEditService.removeTypes(this, types);
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.aspectRatio,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
EntryDataType.address,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void editIptcValues(List<Map<String, dynamic>> iptc, int record, int tag, Set<String> values) {
|
||||||
|
iptc.removeWhere((v) => v['record'] == record && v['tag'] == tag);
|
||||||
|
iptc.add({
|
||||||
|
'record': record,
|
||||||
|
'tag': tag,
|
||||||
|
'values': values.map((v) => utf8.encode(v)).toList(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static bool editCreateDateXmp(List<XmlNode> descriptions, DateTime? date) {
|
||||||
|
return XMP.setAttribute(
|
||||||
|
descriptions,
|
||||||
|
XMP.xmpCreateDate,
|
||||||
|
date != null ? XMP.toXmpDate(date) : null,
|
||||||
|
namespace: Namespaces.xmp,
|
||||||
|
strat: XmpEditStrategy.always,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static bool editTagsXmp(List<XmlNode> descriptions, Set<String> tags) {
|
||||||
|
return XMP.setStringBag(
|
||||||
|
descriptions,
|
||||||
|
XMP.dcSubject,
|
||||||
|
tags,
|
||||||
|
namespace: Namespaces.dc,
|
||||||
|
strat: XmpEditStrategy.always,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static bool editRatingXmp(List<XmlNode> descriptions, int? rating) {
|
||||||
|
bool modified = false;
|
||||||
|
|
||||||
|
modified |= XMP.setAttribute(
|
||||||
|
descriptions,
|
||||||
|
XMP.xmpRating,
|
||||||
|
(rating ?? 0) == 0 ? null : '$rating',
|
||||||
|
namespace: Namespaces.xmp,
|
||||||
|
strat: XmpEditStrategy.always,
|
||||||
|
);
|
||||||
|
|
||||||
|
modified |= XMP.setAttribute(
|
||||||
|
descriptions,
|
||||||
|
XMP.msPhotoRating,
|
||||||
|
XMP.toMsPhotoRating(rating),
|
||||||
|
namespace: Namespaces.microsoftPhoto,
|
||||||
|
strat: XmpEditStrategy.updateIfPresent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static bool removeContainerXmp(List<XmlNode> descriptions) {
|
||||||
|
bool modified = false;
|
||||||
|
|
||||||
|
modified |= XMP.removeElements(
|
||||||
|
descriptions,
|
||||||
|
XMP.containerDirectory,
|
||||||
|
Namespaces.gContainer,
|
||||||
|
);
|
||||||
|
|
||||||
|
modified |= [
|
||||||
|
XMP.gCameraMicroVideo,
|
||||||
|
XMP.gCameraMicroVideoVersion,
|
||||||
|
XMP.gCameraMicroVideoOffset,
|
||||||
|
XMP.gCameraMicroVideoPresentationTimestampUs,
|
||||||
|
XMP.gCameraMotionPhoto,
|
||||||
|
XMP.gCameraMotionPhotoVersion,
|
||||||
|
XMP.gCameraMotionPhotoPresentationTimestampUs,
|
||||||
|
].fold<bool>(modified, (prev, name) {
|
||||||
|
return prev |= XMP.removeElements(
|
||||||
|
descriptions,
|
||||||
|
name,
|
||||||
|
Namespaces.gCamera,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenience methods
|
||||||
|
|
||||||
|
// This method checks whether the item already has a metadata date,
|
||||||
|
// and adds a date (the file modified date) via Exif if possible.
|
||||||
|
// It returns a date if the caller needs to add it via other metadata types (e.g. XMP).
|
||||||
|
Future<DateTime?> _missingDateCheckAndExifEdit(Set<EntryDataType> dataTypes) async {
|
||||||
|
if (path == null) return null;
|
||||||
|
|
||||||
|
// make sure entry is catalogued before we check whether is has a metadata date
|
||||||
|
if (!isCatalogued) {
|
||||||
|
await catalog(background: false, force: false, persist: true);
|
||||||
|
}
|
||||||
|
final dateMillis = catalogMetadata?.dateMillis;
|
||||||
|
if (dateMillis != null && dateMillis > 0) return null;
|
||||||
|
|
||||||
|
late DateTime date;
|
||||||
|
try {
|
||||||
|
date = await File(path!).lastModified();
|
||||||
|
} on FileSystemException catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canEditExif) {
|
||||||
|
final newFields = await metadataEditService.editExifDate(this, DateModifier.setCustom(const {MetadataField.exifDateOriginal}, date));
|
||||||
|
if (newFields.isNotEmpty) {
|
||||||
|
dataTypes.addAll({
|
||||||
|
EntryDataType.basic,
|
||||||
|
EntryDataType.catalog,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DateModifier?> _applyDateModifierToEntry(DateModifier modifier) async {
|
||||||
|
Set<MetadataField> mainMetadataDate() => {canEditExif ? MetadataField.exifDateOriginal : MetadataField.xmpXmpCreateDate};
|
||||||
|
|
||||||
|
switch (modifier.action) {
|
||||||
|
case DateEditAction.copyField:
|
||||||
|
DateTime? date;
|
||||||
|
final source = modifier.copyFieldSource;
|
||||||
|
if (source != null) {
|
||||||
|
switch (source) {
|
||||||
|
case DateFieldSource.fileModifiedDate:
|
||||||
|
try {
|
||||||
|
if (path != null) {
|
||||||
|
final file = File(path!);
|
||||||
|
if (await file.exists()) {
|
||||||
|
date = await file.lastModified();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on FileSystemException catch (_) {}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
date = await metadataFetchService.getDate(this, source.toMetadataField()!);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
|
||||||
|
case DateEditAction.extractFromTitle:
|
||||||
|
final date = parseUnknownDateFormat(bestTitle);
|
||||||
|
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
|
||||||
|
case DateEditAction.setCustom:
|
||||||
|
case DateEditAction.copyItem:
|
||||||
|
return DateModifier.setCustom(mainMetadataDate(), modifier.setDateTime!);
|
||||||
|
case DateEditAction.shift:
|
||||||
|
case DateEditAction.remove:
|
||||||
|
return modifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const xmpCoreKey = 'xmp';
|
||||||
|
static const xmpExtendedKey = 'extendedXmp';
|
||||||
|
|
||||||
|
Future<Map<String, String?>> _editXmp(bool Function(List<XmlNode> descriptions) apply) async {
|
||||||
|
final xmp = await metadataFetchService.getXmp(this);
|
||||||
|
if (xmp == null) {
|
||||||
|
throw Exception('failed to get XMP');
|
||||||
|
}
|
||||||
|
|
||||||
|
final xmpString = xmp.xmpString;
|
||||||
|
final extendedXmpString = xmp.extendedXmpString;
|
||||||
|
|
||||||
|
final editedXmpString = await XMP.edit(
|
||||||
|
xmpString,
|
||||||
|
() => PackageInfo.fromPlatform().then((v) => 'Aves v${v.version}'),
|
||||||
|
apply,
|
||||||
|
);
|
||||||
|
|
||||||
|
final editedXmp = AvesXmp(xmpString: editedXmpString, extendedXmpString: extendedXmpString);
|
||||||
|
return {
|
||||||
|
xmpCoreKey: editedXmp.xmpString,
|
||||||
|
xmpExtendedKey: editedXmp.extendedXmpString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DescriptionField { title, description }
|
53
lib/model/entry/extensions/multipage.dart
Normal file
53
lib/model/entry/extensions/multipage.dart
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/multipage.dart';
|
||||||
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
extension ExtraAvesEntryMultipage on AvesEntry {
|
||||||
|
static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
|
||||||
|
|
||||||
|
bool get isMultiPage => (catalogMetadata?.isMultiPage ?? false) || isBurst;
|
||||||
|
|
||||||
|
bool get isBurst => burstEntries?.isNotEmpty == true;
|
||||||
|
|
||||||
|
// for backward compatibility
|
||||||
|
bool get _isMotionPhotoLegacy => isMultiPage && !isBurst && mimeType == MimeTypes.jpeg;
|
||||||
|
|
||||||
|
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
|
||||||
|
|
||||||
|
String? get burstKey {
|
||||||
|
if (filenameWithoutExtension != null) {
|
||||||
|
final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
|
||||||
|
if (match != null) {
|
||||||
|
return '$directory/${match.group(1)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MultiPageInfo?> getMultiPageInfo() async {
|
||||||
|
if (isBurst) {
|
||||||
|
return MultiPageInfo(
|
||||||
|
mainEntry: this,
|
||||||
|
pages: burstEntries!
|
||||||
|
.mapIndexed((index, entry) => SinglePageInfo(
|
||||||
|
index: index,
|
||||||
|
pageId: entry.id,
|
||||||
|
isDefault: index == 0,
|
||||||
|
uri: entry.uri,
|
||||||
|
mimeType: entry.mimeType,
|
||||||
|
width: entry.width,
|
||||||
|
height: entry.height,
|
||||||
|
rotationDegrees: entry.rotationDegrees,
|
||||||
|
durationMillis: entry.durationMillis,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await metadataFetchService.getMultiPageInfo(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
119
lib/model/entry/extensions/props.dart
Normal file
119
lib/model/entry/extensions/props.dart
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
|
||||||
|
extension ExtraAvesEntryProps on AvesEntry {
|
||||||
|
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
||||||
|
|
||||||
|
bool get isSvg => mimeType == MimeTypes.svg;
|
||||||
|
|
||||||
|
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
|
||||||
|
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
|
||||||
|
|
||||||
|
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
|
||||||
|
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
|
||||||
|
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
|
||||||
|
bool get _supportedByBitmapRegionDecoder =>
|
||||||
|
[
|
||||||
|
MimeTypes.heic,
|
||||||
|
MimeTypes.heif,
|
||||||
|
MimeTypes.jpeg,
|
||||||
|
MimeTypes.png,
|
||||||
|
MimeTypes.webp,
|
||||||
|
MimeTypes.arw,
|
||||||
|
MimeTypes.cr2,
|
||||||
|
MimeTypes.nef,
|
||||||
|
MimeTypes.nrw,
|
||||||
|
MimeTypes.orf,
|
||||||
|
MimeTypes.pef,
|
||||||
|
MimeTypes.raf,
|
||||||
|
MimeTypes.rw2,
|
||||||
|
MimeTypes.srw,
|
||||||
|
].contains(mimeType) &&
|
||||||
|
!isAnimated;
|
||||||
|
|
||||||
|
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
||||||
|
|
||||||
|
bool get useTiles => supportTiling && (width > 4096 || height > 4096);
|
||||||
|
|
||||||
|
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
||||||
|
|
||||||
|
bool get isImage => MimeTypes.isImage(mimeType);
|
||||||
|
|
||||||
|
bool get isVideo => MimeTypes.isVideo(mimeType);
|
||||||
|
|
||||||
|
bool get isAnimated => catalogMetadata?.isAnimated ?? false;
|
||||||
|
|
||||||
|
bool get isGeotiff => catalogMetadata?.isGeotiff ?? false;
|
||||||
|
|
||||||
|
bool get is360 => catalogMetadata?.is360 ?? false;
|
||||||
|
|
||||||
|
bool get isMediaStoreContent => uri.startsWith('content://media/');
|
||||||
|
|
||||||
|
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
|
||||||
|
|
||||||
|
bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
|
||||||
|
|
||||||
|
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent);
|
||||||
|
|
||||||
|
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
||||||
|
|
||||||
|
bool get canEditLocation => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
|
||||||
|
|
||||||
|
bool get canEditTitleDescription => canEdit && canEditXmp;
|
||||||
|
|
||||||
|
bool get canEditRating => canEdit && canEditXmp;
|
||||||
|
|
||||||
|
bool get canEditTags => canEdit && canEditXmp;
|
||||||
|
|
||||||
|
bool get canRotate => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
|
||||||
|
|
||||||
|
bool get canFlip => canEdit && canEditExif;
|
||||||
|
|
||||||
|
bool get canEditExif => MimeTypes.canEditExif(mimeType);
|
||||||
|
|
||||||
|
bool get canEditIptc => MimeTypes.canEditIptc(mimeType);
|
||||||
|
|
||||||
|
bool get canEditXmp => MimeTypes.canEditXmp(mimeType);
|
||||||
|
|
||||||
|
bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType);
|
||||||
|
|
||||||
|
static const ratioSeparator = '\u2236';
|
||||||
|
static const resolutionSeparator = ' \u00D7 ';
|
||||||
|
|
||||||
|
bool get isSized => width > 0 && height > 0;
|
||||||
|
|
||||||
|
String get resolutionText {
|
||||||
|
final ws = width;
|
||||||
|
final hs = height;
|
||||||
|
return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get aspectRatioText {
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
final gcd = width.gcd(height);
|
||||||
|
final w = width ~/ gcd;
|
||||||
|
final h = height ~/ gcd;
|
||||||
|
return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h';
|
||||||
|
} else {
|
||||||
|
return '?$ratioSeparator?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Size videoDisplaySize(double sar) {
|
||||||
|
final size = displaySize;
|
||||||
|
if (sar != 1) {
|
||||||
|
final dar = displayAspectRatio * sar;
|
||||||
|
final w = size.width;
|
||||||
|
final h = size.height;
|
||||||
|
if (w >= h) return Size(w, w / dar);
|
||||||
|
if (h > w) return Size(h * dar, h);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get megaPixels => (width * height / 1000000).round();
|
||||||
|
}
|
6
lib/model/entry/origins.dart
Normal file
6
lib/model/entry/origins.dart
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
class EntryOrigins {
|
||||||
|
static const int mediaStoreContent = 0;
|
||||||
|
static const int unknownContent = 1;
|
||||||
|
static const int file = 2;
|
||||||
|
static const int vault = 3;
|
||||||
|
}
|
38
lib/model/entry/sort.dart
Normal file
38
lib/model/entry/sort.dart
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/utils/time_utils.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class AvesEntrySort {
|
||||||
|
// compare by:
|
||||||
|
// 1) title ascending
|
||||||
|
// 2) extension ascending
|
||||||
|
static int compareByName(AvesEntry a, AvesEntry b) {
|
||||||
|
final c = compareAsciiUpperCaseNatural(a.bestTitle ?? '', b.bestTitle ?? '');
|
||||||
|
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare by:
|
||||||
|
// 1) date descending
|
||||||
|
// 2) name descending
|
||||||
|
static int compareByDate(AvesEntry a, AvesEntry b) {
|
||||||
|
var c = (b.bestDate ?? epoch).compareTo(a.bestDate ?? epoch);
|
||||||
|
if (c != 0) return c;
|
||||||
|
return compareByName(b, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare by:
|
||||||
|
// 1) rating descending
|
||||||
|
// 2) date descending
|
||||||
|
static int compareByRating(AvesEntry a, AvesEntry b) {
|
||||||
|
final c = b.rating.compareTo(a.rating);
|
||||||
|
return c != 0 ? c : compareByDate(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare by:
|
||||||
|
// 1) size descending
|
||||||
|
// 2) date descending
|
||||||
|
static int compareBySize(AvesEntry a, AvesEntry b) {
|
||||||
|
final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0);
|
||||||
|
return c != 0 ? c : compareByDate(a, b);
|
||||||
|
}
|
||||||
|
}
|
0
lib/model/entry_extensions/geo.dart
Normal file
0
lib/model/entry_extensions/geo.dart
Normal file
0
lib/model/entry_extensions/multipage.dart
Normal file
0
lib/model/entry_extensions/multipage.dart
Normal file
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/l10n/l10n.dart';
|
import 'package:aves/l10n/l10n.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/aspect_ratio.dart';
|
import 'package:aves/model/filters/aspect_ratio.dart';
|
||||||
import 'package:aves/model/filters/coordinate.dart';
|
import 'package:aves/model/filters/coordinate.dart';
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -2,8 +2,8 @@ import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry_images.dart';
|
import 'package:aves/model/entry/extensions/images.dart';
|
||||||
import 'package:aves/ref/geotiff.dart';
|
import 'package:aves/ref/geotiff.dart';
|
||||||
import 'package:aves/utils/math_utils.dart';
|
import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:aves_map/aves_map.dart';
|
import 'package:aves_map/aves_map.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@ -18,12 +17,11 @@ extension ExtraVideoLoopMode on VideoLoopMode {
|
||||||
|
|
||||||
static const shortVideoThreshold = Duration(seconds: 30);
|
static const shortVideoThreshold = Duration(seconds: 30);
|
||||||
|
|
||||||
bool shouldLoop(AvesEntry entry) {
|
bool shouldLoop(int? durationMillis) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case VideoLoopMode.never:
|
case VideoLoopMode.never:
|
||||||
return false;
|
return false;
|
||||||
case VideoLoopMode.shortOnly:
|
case VideoLoopMode.shortOnly:
|
||||||
final durationMillis = entry.durationMillis;
|
|
||||||
return durationMillis != null ? durationMillis < shortVideoThreshold.inMilliseconds : false;
|
return durationMillis != null ? durationMillis < shortVideoThreshold.inMilliseconds : false;
|
||||||
case VideoLoopMode.always:
|
case VideoLoopMode.always:
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
|
|
@ -2,7 +2,9 @@ import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/entry/sort.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
|
@ -190,7 +192,7 @@ class CollectionLens with ChangeNotifier {
|
||||||
final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey();
|
final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey();
|
||||||
byBurstKey.forEach((burstKey, entries) {
|
byBurstKey.forEach((burstKey, entries) {
|
||||||
if (entries.length > 1) {
|
if (entries.length > 1) {
|
||||||
entries.sort(AvesEntry.compareByName);
|
entries.sort(AvesEntrySort.compareByName);
|
||||||
final mainEntry = entries.first;
|
final mainEntry = entries.first;
|
||||||
final burstEntry = mainEntry.copyWith(burstEntries: entries);
|
final burstEntry = mainEntry.copyWith(burstEntries: entries);
|
||||||
|
|
||||||
|
@ -209,16 +211,16 @@ class CollectionLens with ChangeNotifier {
|
||||||
|
|
||||||
switch (sortFactor) {
|
switch (sortFactor) {
|
||||||
case EntrySortFactor.date:
|
case EntrySortFactor.date:
|
||||||
_filteredSortedEntries.sort(AvesEntry.compareByDate);
|
_filteredSortedEntries.sort(AvesEntrySort.compareByDate);
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.name:
|
case EntrySortFactor.name:
|
||||||
_filteredSortedEntries.sort(AvesEntry.compareByName);
|
_filteredSortedEntries.sort(AvesEntrySort.compareByName);
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.rating:
|
case EntrySortFactor.rating:
|
||||||
_filteredSortedEntries.sort(AvesEntry.compareByRating);
|
_filteredSortedEntries.sort(AvesEntrySort.compareByRating);
|
||||||
break;
|
break;
|
||||||
case EntrySortFactor.size:
|
case EntrySortFactor.size:
|
||||||
_filteredSortedEntries.sort(AvesEntry.compareBySize);
|
_filteredSortedEntries.sort(AvesEntrySort.compareBySize);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (sortReverse) {
|
if (sortReverse) {
|
||||||
|
|
|
@ -2,7 +2,10 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
|
import 'package:aves/model/entry/sort.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
@ -105,7 +108,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<AvesEntry> get sortedEntriesByDate {
|
List<AvesEntry> get sortedEntriesByDate {
|
||||||
_sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntry.compareByDate));
|
_sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntrySort.compareByDate));
|
||||||
return _sortedEntriesByDate!;
|
return _sortedEntriesByDate!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/utils/collection_utils.dart';
|
import 'package:aves/utils/collection_utils.dart';
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/geo/countries.dart';
|
import 'package:aves/geo/countries.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/metadata/address.dart';
|
import 'package:aves/model/metadata/address.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/utils/collection_utils.dart';
|
import 'package:aves/utils/collection_utils.dart';
|
||||||
|
|
|
@ -2,7 +2,8 @@ import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/origins.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/analysis_controller.dart';
|
import 'package:aves/model/source/analysis_controller.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/model/source/analysis_controller.dart';
|
import 'package:aves/model/source/analysis_controller.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/model/video/channel_layouts.dart';
|
import 'package:aves/model/video/channel_layouts.dart';
|
||||||
import 'package:aves/model/video/codecs.dart';
|
import 'package:aves/model/video/codecs.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/metadata/enums/enums.dart';
|
import 'package:aves/model/metadata/enums/enums.dart';
|
||||||
import 'package:aves/services/common/image_op_events.dart';
|
import 'package:aves/services/common/image_op_events.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/output_buffer.dart';
|
import 'package:aves/services/common/output_buffer.dart';
|
||||||
import 'package:aves/services/common/service_policy.dart';
|
import 'package:aves/services/common/service_policy.dart';
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/services/common/optional_event_channel.dart';
|
import 'package:aves/services/common/optional_event_channel.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves_video/aves_video.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -12,6 +13,7 @@ abstract class MediaSessionService {
|
||||||
Stream<MediaCommandEvent> get mediaCommands;
|
Stream<MediaCommandEvent> get mediaCommands;
|
||||||
|
|
||||||
Future<void> update({
|
Future<void> update({
|
||||||
|
required AvesEntry entry,
|
||||||
required AvesVideoController controller,
|
required AvesVideoController controller,
|
||||||
required bool canSkipToNext,
|
required bool canSkipToNext,
|
||||||
required bool canSkipToPrevious,
|
required bool canSkipToPrevious,
|
||||||
|
@ -43,11 +45,11 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> update({
|
Future<void> update({
|
||||||
|
required AvesEntry entry,
|
||||||
required AvesVideoController controller,
|
required AvesVideoController controller,
|
||||||
required bool canSkipToNext,
|
required bool canSkipToNext,
|
||||||
required bool canSkipToPrevious,
|
required bool canSkipToPrevious,
|
||||||
}) async {
|
}) async {
|
||||||
final entry = controller.entry;
|
|
||||||
try {
|
try {
|
||||||
await _platformObject.invokeMethod('update', <String, dynamic>{
|
await _platformObject.invokeMethod('update', <String, dynamic>{
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:streams_channel/streams_channel.dart';
|
import 'package:streams_channel/streams_channel.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
import 'package:aves/model/metadata/enums/enums.dart';
|
import 'package:aves/model/metadata/enums/enums.dart';
|
||||||
import 'package:aves/model/metadata/enums/metadata_type.dart';
|
import 'package:aves/model/metadata/enums/metadata_type.dart';
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/geotiff.dart';
|
import 'package:aves/model/geotiff.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/model/metadata/fields.dart';
|
import 'package:aves/model/metadata/fields.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/string_utils.dart';
|
import 'package:aves/utils/string_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_flavor.dart';
|
import 'package:aves/app_flavor.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/sort.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
@ -80,7 +81,7 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
|
||||||
entries.shuffle();
|
entries.shuffle();
|
||||||
break;
|
break;
|
||||||
case WidgetDisplayedItem.mostRecent:
|
case WidgetDisplayedItem.mostRecent:
|
||||||
entries.sort(AvesEntry.compareByDate);
|
entries.sort(AvesEntrySort.compareByDate);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
final entry = entries.firstOrNull;
|
final entry = entries.firstOrNull;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
import 'package:aves/model/filters/trash.dart';
|
import 'package:aves/model/filters/trash.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
import 'package:aves/model/filters/trash.dart';
|
import 'package:aves/model/filters/trash.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/rating.dart';
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
|
|
@ -4,8 +4,10 @@ import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry_metadata_edition.dart';
|
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/section_keys.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums/enums.dart';
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/section_keys.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
import 'package:aves/widgets/collection/grid/headers/any.dart';
|
import 'package:aves/widgets/collection/grid/headers/any.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/enums/enums.dart';
|
import 'package:aves/model/source/enums/enums.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/actions/share_actions.dart';
|
import 'package:aves/model/actions/share_actions.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart';
|
import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart';
|
||||||
import 'package:aves/widgets/common/action_controls/quick_choosers/share_chooser.dart';
|
import 'package:aves/widgets/common/action_controls/quick_choosers/share_chooser.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves_video/aves_video.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MuteToggler extends StatelessWidget {
|
class MuteToggler extends StatelessWidget {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves_video/aves_video.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry_metadata_edition.dart';
|
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/placeholder.dart';
|
import 'package:aves/model/filters/placeholder.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
|
|
|
@ -3,7 +3,9 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/trash.dart';
|
import 'package:aves/model/filters/trash.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/collection_utils.dart';
|
import 'package:aves/utils/collection_utils.dart';
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_icons.dart';
|
import 'package:aves/widgets/common/identity/aves_icons.dart';
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/vaults/vaults.dart';
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
|
|
@ -2,8 +2,10 @@ import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry_images.dart';
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/images.dart';
|
||||||
|
import 'package:aves/model/entry/sort.dart';
|
||||||
import 'package:aves/model/settings/enums/map_style.dart';
|
import 'package:aves/model/settings/enums/map_style.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
@ -343,7 +345,7 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
|
|
||||||
ZoomedBounds? _initBoundsForEntries({required List<AvesEntry> entries, int? recentCount}) {
|
ZoomedBounds? _initBoundsForEntries({required List<AvesEntry> entries, int? recentCount}) {
|
||||||
if (recentCount != null) {
|
if (recentCount != null) {
|
||||||
entries = List.of(entries)..sort(AvesEntry.compareByDate);
|
entries = List.of(entries)..sort(AvesEntrySort.compareByDate);
|
||||||
entries = entries.take(recentCount).toList();
|
entries = entries.take(recentCount).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
import 'package:aves/widgets/common/grid/overlay.dart';
|
import 'package:aves/widgets/common/grid/overlay.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/mime_utils.dart';
|
import 'package:aves/utils/mime_utils.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
|
@ -2,8 +2,9 @@ import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry_images.dart';
|
import 'package:aves/model/entry/extensions/images.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||||
import 'package:aves/model/settings/enums/entry_background.dart';
|
import 'package:aves/model/settings/enums/entry_background.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/known_extent_scroll_physics.dart';
|
import 'package:aves/widgets/common/behaviour/known_extent_scroll_physics.dart';
|
||||||
import 'package:aves/widgets/common/grid/theme.dart';
|
import 'package:aves/widgets/common/grid/theme.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/metadata/address.dart';
|
import 'package:aves/model/metadata/address.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/metadata/enums/enums.dart';
|
import 'package:aves/model/metadata/enums/enums.dart';
|
||||||
import 'package:aves/model/metadata/enums/length_unit.dart';
|
import 'package:aves/model/metadata/enums/length_unit.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
@ -148,7 +149,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(AvesEntry.resolutionSeparator),
|
const Text(ExtraAvesEntryProps.resolutionSeparator),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
import 'package:aves/model/metadata/enums/date_edit_action.dart';
|
import 'package:aves/model/metadata/enums/date_edit_action.dart';
|
||||||
import 'package:aves/model/metadata/enums/date_field_source.dart';
|
import 'package:aves/model/metadata/enums/date_field_source.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry_metadata_edition.dart';
|
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||||
import 'package:aves/widgets/common/basic/labeled_checkbox.dart';
|
import 'package:aves/widgets/common/basic/labeled_checkbox.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry_metadata_edition.dart';
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/metadata_edition.dart';
|
||||||
import 'package:aves/model/metadata/enums/enums.dart';
|
import 'package:aves/model/metadata/enums/enums.dart';
|
||||||
import 'package:aves/model/metadata/enums/location_edit_action.dart';
|
import 'package:aves/model/metadata/enums/location_edit_action.dart';
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/naming_pattern.dart';
|
import 'package:aves/model/naming_pattern.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/placeholder.dart';
|
import 'package:aves/model/filters/placeholder.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/ref/languages.dart';
|
import 'package:aves/ref/languages.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves_video/aves_video.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
|
||||||
final w = stream.width;
|
final w = stream.width;
|
||||||
final h = stream.height;
|
final h = stream.height;
|
||||||
if (w != null && h != null) {
|
if (w != null && h != null) {
|
||||||
return '$common • $w${AvesEntry.resolutionSeparator}$h';
|
return '$common • $w${ExtraAvesEntryProps.resolutionSeparator}$h';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return common;
|
return common;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/chip_set_actions.dart';
|
import 'package:aves/model/actions/chip_set_actions.dart';
|
||||||
import 'package:aves/model/actions/move_type.dart';
|
import 'package:aves/model/actions/move_type.dart';
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue