diff --git a/lib/model/covers.dart b/lib/model/covers.dart index 601a07bd5..0eb13eb55 100644 --- a/lib/model/covers.dart +++ b/lib/model/covers.dart @@ -1,6 +1,6 @@ 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/filters.dart'; import 'package:aves/model/vaults/vaults.dart'; diff --git a/lib/model/db/db_metadata.dart b/lib/model/db/db_metadata.dart index bf4f677ae..524efd299 100644 --- a/lib/model/db/db_metadata.dart +++ b/lib/model/db/db_metadata.dart @@ -1,5 +1,5 @@ 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/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; diff --git a/lib/model/db/db_metadata_sqflite.dart b/lib/model/db/db_metadata_sqflite.dart index 9bc6a8885..e8903a076 100644 --- a/lib/model/db/db_metadata_sqflite.dart +++ b/lib/model/db/db_metadata_sqflite.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:aves/model/covers.dart'; import 'package:aves/model/db/db_metadata.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/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; diff --git a/lib/model/entry_cache.dart b/lib/model/entry/cache.dart similarity index 100% rename from lib/model/entry_cache.dart rename to lib/model/entry/cache.dart diff --git a/lib/model/entry_dirs.dart b/lib/model/entry/dirs.dart similarity index 100% rename from lib/model/entry_dirs.dart rename to lib/model/entry/dirs.dart diff --git a/lib/model/entry.dart b/lib/model/entry/entry.dart similarity index 55% rename from lib/model/entry.dart rename to lib/model/entry/entry.dart index 794ed6f6e..cfa81b453 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry/entry.dart @@ -2,51 +2,42 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; -import 'package:aves/geo/countries.dart'; -import 'package:aves/model/entry_cache.dart'; -import 'package:aves/model/entry_dirs.dart'; -import 'package:aves/model/favourites.dart'; -import 'package:aves/model/geotiff.dart'; +import 'package:aves/model/entry/cache.dart'; +import 'package:aves/model/entry/dirs.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.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/video/metadata.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/geocoding_service.dart'; -import 'package:aves/services/metadata/svg_metadata_service.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/time_utils.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; -import 'package:country_code/country_code.dart'; import 'package:flutter/foundation.dart'; -import 'package:latlong2/latlong.dart'; enum EntryDataType { basic, aspectRatio, catalog, address, references } -class EntryOrigins { - static const int mediaStoreContent = 0; - 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 +class AvesEntry with AvesEntryBase { + @override int id; + + @override String uri; + + @override + int? pageId; + + @override + int? sizeBytes; + String? _path, _filename, _extension, _sourceTitle; EntryDir? _directory; - int? pageId, contentId; + int? contentId; final String sourceMimeType; int width, height, sourceRotationDegrees; - int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; + int? dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; bool trashed; int origin; @@ -57,7 +48,11 @@ class AvesEntry { List? burstEntries; - final AChangeNotifier visualChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier(); + @override + final AChangeNotifier visualChangeNotifier = AChangeNotifier(); + + final AChangeNotifier metadataChangeNotifier = AChangeNotifier(); + final AChangeNotifier addressChangeNotifier = AChangeNotifier(); AvesEntry({ required int? id, @@ -243,140 +238,8 @@ class AvesEntry { // so we use the one found during cataloguing if possible 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 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? get bestDate { @@ -386,6 +249,7 @@ class AvesEntry { int get rating => _catalogMetadata?.rating ?? 0; + @override int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees; set rotationDegrees(int rotationDegrees) { @@ -397,6 +261,27 @@ class AvesEntry { 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; set sourceTitle(String? sourceTitle) { @@ -423,6 +308,7 @@ class AvesEntry { return d == null ? null : DateTime(d.year, d.month, d.day); } + @override int? get durationMillis => _durationMillis; set durationMillis(int? durationMillis) { @@ -459,8 +345,6 @@ class AvesEntry { // derived from Google reverse geocoding addresses bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3; - LatLng? get latLng => hasGps ? LatLng(_catalogMetadata!.latitude!, _catalogMetadata!.longitude!) : null; - Set? _tags; Set get tags { @@ -504,53 +388,6 @@ class AvesEntry { addressDetails = null; } - Future 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; set addressDetails(AddressDetails? newAddress) { @@ -558,79 +395,6 @@ class AvesEntry { addressChangeNotifier.notify(); } - Future 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 _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 locatePlace({required bool background, required bool force, required Locale geocoderLocale}) async { - if (!hasGps || (hasFineAddress && !force)) return; - try { - Future> 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 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 { // `admin area` examples: Seoul, Geneva, null // `locality` examples: Mapo-gu, Geneva, Annecy @@ -732,107 +496,4 @@ class AvesEntry { visualChangeNotifier.notify(); } } - - // favourites - - Future toggleFavourite() async { - if (isFavourite) { - await removeFromFavourites(); - } else { - await addToFavourites(); - } - } - - Future addToFavourites() async { - if (!isFavourite) { - await favourites.add({this}); - } - } - - Future 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 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); - } } diff --git a/lib/model/entry/extensions/catalog.dart b/lib/model/entry/extensions/catalog.dart new file mode 100644 index 000000000..897f017a8 --- /dev/null +++ b/lib/model/entry/extensions/catalog.dart @@ -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 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, + ); + } + } + } + } + } +} diff --git a/lib/model/entry/extensions/favourites.dart b/lib/model/entry/extensions/favourites.dart new file mode 100644 index 000000000..22ca36f0b --- /dev/null +++ b/lib/model/entry/extensions/favourites.dart @@ -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 toggleFavourite() async { + if (isFavourite) { + await removeFromFavourites(); + } else { + await addToFavourites(); + } + } + + Future addToFavourites() async { + if (!isFavourite) { + await favourites.add({this}); + } + } + + Future removeFromFavourites() async { + if (isFavourite) { + await favourites.removeEntries({this}); + } + } +} diff --git a/lib/model/entry/extensions/images.dart b/lib/model/entry/extensions/images.dart new file mode 100644 index 000000000..46e18a337 --- /dev/null +++ b/lib/model/entry/extensions/images.dart @@ -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 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 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(1, sample); + } +} diff --git a/lib/model/entry/extensions/info.dart b/lib/model/entry/extensions/info.dart new file mode 100644 index 000000000..4087d10ac --- /dev/null +++ b/lib/model/entry/extensions/info.dart @@ -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'^((?.*?)/)?(?.*?)(\[(?\d+)\])?$'); + + Future>> 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> _getStreamDirectories(BuildContext context) async { + final directories = []; + 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(); + 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(); + 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 = >{}; + 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 _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; + } +} diff --git a/lib/model/entry/extensions/location.dart b/lib/model/entry/extensions/location.dart new file mode 100644 index 000000000..97f477c15 --- /dev/null +++ b/lib/model/entry/extensions/location.dart @@ -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 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 _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 locatePlace({required bool background, required bool force, required Locale geocoderLocale}) async { + if (!hasGps || (hasFineAddress && !force)) return; + try { + Future> 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 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; + } +} diff --git a/lib/model/entry/extensions/metadata_edition.dart b/lib/model/entry/extensions/metadata_edition.dart new file mode 100644 index 000000000..c283b873f --- /dev/null +++ b/lib/model/entry/extensions/metadata_edition.dart @@ -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> editDate(DateModifier userModifier) async { + final dataTypes = {}; + + 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> editLocation(LatLng? latLng) async { + final dataTypes = {}; + final metadata = {}; + + final missingDate = await _missingDateCheckAndExifEdit(dataTypes); + + if (canEditExif) { + // clear every GPS field + final exifFields = Map.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.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 = {}; + + 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.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> _changeExifOrientation(Future> Function() apply) async { + final dataTypes = {}; + + 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> _rotateMp4(int rotationDegrees) async { + final dataTypes = {}; + + final missingDate = await _missingDateCheckAndExifEdit(dataTypes); + + final mp4Fields = { + 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.mp4: Map.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> 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> 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> editTitleDescription(Map fields) async { + final dataTypes = {}; + final metadata = {}; + + 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> editTags(Set tags) async { + final dataTypes = {}; + final metadata = {}; + + 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> editRating(int? rating) async { + final dataTypes = {}; + final metadata = {}; + + 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> removeTrailerVideo() async { + final dataTypes = {}; + final metadata = {}; + + 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> removeMetadata(Set types) async { + final dataTypes = {}; + + 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> iptc, int record, int tag, Set 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 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 descriptions, Set tags) { + return XMP.setStringBag( + descriptions, + XMP.dcSubject, + tags, + namespace: Namespaces.dc, + strat: XmpEditStrategy.always, + ); + } + + @visibleForTesting + static bool editRatingXmp(List 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 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(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 _missingDateCheckAndExifEdit(Set 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 _applyDateModifierToEntry(DateModifier modifier) async { + Set 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> _editXmp(bool Function(List 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 } diff --git a/lib/model/entry/extensions/multipage.dart b/lib/model/entry/extensions/multipage.dart new file mode 100644 index 000000000..f77f252b1 --- /dev/null +++ b/lib/model/entry/extensions/multipage.dart @@ -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 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); + } + } +} diff --git a/lib/model/entry/extensions/props.dart b/lib/model/entry/extensions/props.dart new file mode 100644 index 000000000..ed1c3ef1c --- /dev/null +++ b/lib/model/entry/extensions/props.dart @@ -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(); +} diff --git a/lib/model/entry/origins.dart b/lib/model/entry/origins.dart new file mode 100644 index 000000000..efc35496a --- /dev/null +++ b/lib/model/entry/origins.dart @@ -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; +} diff --git a/lib/model/entry/sort.dart b/lib/model/entry/sort.dart new file mode 100644 index 000000000..f15521bc0 --- /dev/null +++ b/lib/model/entry/sort.dart @@ -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); + } +} diff --git a/lib/model/entry_extensions/geo.dart b/lib/model/entry_extensions/geo.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/model/entry_images.dart b/lib/model/entry_extensions/images.dart similarity index 100% rename from lib/model/entry_images.dart rename to lib/model/entry_extensions/images.dart diff --git a/lib/model/entry_info.dart b/lib/model/entry_extensions/info.dart similarity index 100% rename from lib/model/entry_info.dart rename to lib/model/entry_extensions/info.dart diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_extensions/metadata_edition.dart similarity index 100% rename from lib/model/entry_metadata_edition.dart rename to lib/model/entry_extensions/metadata_edition.dart diff --git a/lib/model/entry_extensions/multipage.dart b/lib/model/entry_extensions/multipage.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index d994e31cc..8c5eeea57 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -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/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; diff --git a/lib/model/filters/aspect_ratio.dart b/lib/model/filters/aspect_ratio.dart index 92ee97279..a9a86c90b 100644 --- a/lib/model/filters/aspect_ratio.dart +++ b/lib/model/filters/aspect_ratio.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/query.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index 704d6bac3..2c822fecf 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -1,4 +1,5 @@ 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/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/enums.dart'; diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 7a028411c..413da4c86 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.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/theme/colors.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index a043214b8..212b6c92d 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -1,7 +1,7 @@ import 'dart:convert'; 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/aspect_ratio.dart'; import 'package:aves/model/filters/coordinate.dart'; diff --git a/lib/model/filters/placeholder.dart b/lib/model/filters/placeholder.dart index e0be8e084..d2438e762 100644 --- a/lib/model/filters/placeholder.dart +++ b/lib/model/filters/placeholder.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/settings/settings.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index b82a2ab1c..bf865cb07 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.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/theme/colors.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/model/filters/trash.dart b/lib/model/filters/trash.dart index a378a8668..c995f95b2 100644 --- a/lib/model/filters/trash.dart +++ b/lib/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/filters.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 3ecb278f7..9eafa93b3 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.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/theme/colors.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/model/geotiff.dart b/lib/model/geotiff.dart index dc3ab8deb..a557039ae 100644 --- a/lib/model/geotiff.dart +++ b/lib/model/geotiff.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/ref/geotiff.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves_map/aves_map.dart'; diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index b28945784..278a62e6a 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.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/services/common/services.dart'; import 'package:collection/collection.dart'; diff --git a/lib/model/naming_pattern.dart b/lib/model/naming_pattern.dart index 2cbe98dfc..e24e5221f 100644 --- a/lib/model/naming_pattern.dart +++ b/lib/model/naming_pattern.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:flutter/material.dart'; import 'package:intl/intl.dart'; diff --git a/lib/model/settings/enums/video_loop_mode.dart b/lib/model/settings/enums/video_loop_mode.dart index 0a6cba0a9..255e9ccde 100644 --- a/lib/model/settings/enums/video_loop_mode.dart +++ b/lib/model/settings/enums/video_loop_mode.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/entry.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; @@ -18,12 +17,11 @@ extension ExtraVideoLoopMode on VideoLoopMode { static const shortVideoThreshold = Duration(seconds: 30); - bool shouldLoop(AvesEntry entry) { + bool shouldLoop(int? durationMillis) { switch (this) { case VideoLoopMode.never: return false; case VideoLoopMode.shortOnly: - final durationMillis = entry.durationMillis; return durationMillis != null ? durationMillis < shortVideoThreshold.inMilliseconds : false; case VideoLoopMode.always: return true; diff --git a/lib/model/settings/enums/widget_shape.dart b/lib/model/settings/enums/widget_shape.dart index 19374bfed..7bac91601 100644 --- a/lib/model/settings/enums/widget_shape.dart +++ b/lib/model/settings/enums/widget_shape.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/settings/enums/enums.dart'; import 'package:flutter/material.dart'; diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index fae997994..7b4e9c48c 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.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/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 5c9730444..194236e83 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'dart:collection'; 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/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; @@ -190,7 +192,7 @@ class CollectionLens with ChangeNotifier { final byBurstKey = groupBy(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey(); byBurstKey.forEach((burstKey, entries) { if (entries.length > 1) { - entries.sort(AvesEntry.compareByName); + entries.sort(AvesEntrySort.compareByName); final mainEntry = entries.first; final burstEntry = mainEntry.copyWith(burstEntries: entries); @@ -209,16 +211,16 @@ class CollectionLens with ChangeNotifier { switch (sortFactor) { case EntrySortFactor.date: - _filteredSortedEntries.sort(AvesEntry.compareByDate); + _filteredSortedEntries.sort(AvesEntrySort.compareByDate); break; case EntrySortFactor.name: - _filteredSortedEntries.sort(AvesEntry.compareByName); + _filteredSortedEntries.sort(AvesEntrySort.compareByName); break; case EntrySortFactor.rating: - _filteredSortedEntries.sort(AvesEntry.compareByRating); + _filteredSortedEntries.sort(AvesEntrySort.compareByRating); break; case EntrySortFactor.size: - _filteredSortedEntries.sort(AvesEntry.compareBySize); + _filteredSortedEntries.sort(AvesEntrySort.compareBySize); break; } if (sortReverse) { diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index e31296c46..e2925ac74 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -2,7 +2,10 @@ import 'dart:async'; import 'package:aves/model/actions/move_type.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/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; @@ -105,7 +108,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place @override List get sortedEntriesByDate { - _sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntry.compareByDate)); + _sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntrySort.compareByDate)); return _sortedEntriesByDate!; } diff --git a/lib/model/source/events.dart b/lib/model/source/events.dart index eed59df63..2cd779f31 100644 --- a/lib/model/source/events.dart +++ b/lib/model/source/events.dart @@ -1,5 +1,5 @@ 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'; @immutable diff --git a/lib/model/source/location/country.dart b/lib/model/source/location/country.dart index a6de50f6a..2fcce4db0 100644 --- a/lib/model/source/location/country.dart +++ b/lib/model/source/location/country.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/source/collection_source.dart'; import 'package:aves/utils/collection_utils.dart'; diff --git a/lib/model/source/location/location.dart b/lib/model/source/location/location.dart index 80703a5b6..75fec0ede 100644 --- a/lib/model/source/location/location.dart +++ b/lib/model/source/location/location.dart @@ -1,7 +1,8 @@ import 'dart:math'; 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/metadata/address.dart'; import 'package:aves/model/settings/settings.dart'; diff --git a/lib/model/source/location/place.dart b/lib/model/source/location/place.dart index 342202990..a0b1e3ab6 100644 --- a/lib/model/source/location/place.dart +++ b/lib/model/source/location/place.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/source/collection_source.dart'; import 'package:aves/utils/collection_utils.dart'; diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 22595cd36..4e1c17f2d 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'dart:math'; 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/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index e1ebd33e6..6b8dcd682 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.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/metadata/catalog.dart'; import 'package:aves/model/source/analysis_controller.dart'; diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index 7245ca79b..73c38abae 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -1,6 +1,6 @@ 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/video/channel_layouts.dart'; import 'package:aves/model/video/codecs.dart'; diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 1db5487ad..cf181a68b 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.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/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index 29d2b8627..9acb772da 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.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/services/common/services.dart'; import 'package:flutter/services.dart'; diff --git a/lib/services/media/embedded_data_service.dart b/lib/services/media/embedded_data_service.dart index 33f024ba2..d4a668176 100644 --- a/lib/services/media/embedded_data_service.dart +++ b/lib/services/media/embedded_data_service.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/utils/constants.dart'; import 'package:flutter/services.dart'; diff --git a/lib/services/media/media_edit_service.dart b/lib/services/media/media_edit_service.dart index 6374a1941..0ef3ba684 100644 --- a/lib/services/media/media_edit_service.dart +++ b/lib/services/media/media_edit_service.dart @@ -1,6 +1,6 @@ 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/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart index e50f161d8..4d6a9c4cd 100644 --- a/lib/services/media/media_fetch_service.dart +++ b/lib/services/media/media_fetch_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; 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/services/common/output_buffer.dart'; import 'package:aves/services/common/service_policy.dart'; diff --git a/lib/services/media/media_session_service.dart b/lib/services/media/media_session_service.dart index 643c19297..ed645eadb 100644 --- a/lib/services/media/media_session_service.dart +++ b/lib/services/media/media_session_service.dart @@ -1,8 +1,9 @@ import 'dart:async'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/common/optional_event_channel.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:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -12,6 +13,7 @@ abstract class MediaSessionService { Stream get mediaCommands; Future update({ + required AvesEntry entry, required AvesVideoController controller, required bool canSkipToNext, required bool canSkipToPrevious, @@ -43,11 +45,11 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable { @override Future update({ + required AvesEntry entry, required AvesVideoController controller, required bool canSkipToNext, required bool canSkipToPrevious, }) async { - final entry = controller.entry; try { await _platformObject.invokeMethod('update', { 'uri': entry.uri, diff --git a/lib/services/media/media_store_service.dart b/lib/services/media/media_store_service.dart index 203fd2944..ab762d13a 100644 --- a/lib/services/media/media_store_service.dart +++ b/lib/services/media/media_store_service.dart @@ -1,6 +1,6 @@ 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:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart index 9816fd9a2..53eb204a4 100644 --- a/lib/services/metadata/metadata_edit_service.dart +++ b/lib/services/metadata/metadata_edit_service.dart @@ -1,6 +1,6 @@ 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/enums/enums.dart'; import 'package:aves/model/metadata/enums/metadata_type.dart'; diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index 16c52177f..4998a1b6c 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.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/metadata/catalog.dart'; import 'package:aves/model/metadata/fields.dart'; diff --git a/lib/services/metadata/svg_metadata_service.dart b/lib/services/metadata/svg_metadata_service.dart index a792ef028..882b3527b 100644 --- a/lib/services/metadata/svg_metadata_service.dart +++ b/lib/services/metadata/svg_metadata_service.dart @@ -1,6 +1,6 @@ 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/utils/string_utils.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widget_common.dart b/lib/widget_common.dart index e2316fb3f..c111a54b5 100644 --- a/lib/widget_common.dart +++ b/lib/widget_common.dart @@ -1,7 +1,8 @@ import 'dart:async'; 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/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -80,7 +81,7 @@ Future _getWidgetEntry(int widgetId, bool reuseEntry) async { entries.shuffle(); break; case WidgetDisplayedItem.mostRecent: - entries.sort(AvesEntry.compareByDate); + entries.sort(AvesEntrySort.compareByDate); break; } final entry = entries.firstOrNull; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index d9206938a..de01c21f4 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:aves/app_mode.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/query.dart'; import 'package:aves/model/filters/trash.dart'; diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 15d424e05..f1d78f0b5 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -1,7 +1,7 @@ import 'dart:async'; 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/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 0251638a3..0c3a1ac0e 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,7 +1,7 @@ import 'dart:async'; 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/query.dart'; import 'package:aves/model/filters/trash.dart'; diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index d04670818..6c5bf4bb6 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.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/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index f6f0a6545..3f690f478 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.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/move_type.dart'; import 'package:aves/model/device.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_metadata_edition.dart'; +import 'package:aves/model/entry/entry.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/filters/filters.dart'; import 'package:aves/model/metadata/date_modifier.dart'; diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index 85fb41b32..dc15e9bfc 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -1,5 +1,5 @@ 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/section_keys.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index c3949add9..f3333bda7 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -1,6 +1,6 @@ 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_source.dart'; import 'package:aves/model/source/enums/enums.dart'; diff --git a/lib/widgets/collection/grid/list_details.dart b/lib/widgets/collection/grid/list_details.dart index f71ad7d0b..456ffecc1 100644 --- a/lib/widgets/collection/grid/list_details.dart +++ b/lib/widgets/collection/grid/list_details.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/settings.dart'; import 'package:aves/theme/format.dart'; diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index 276861e32..09a8524f4 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.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/section_keys.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart'; diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index 64eddc2c0..38d5bc5b8 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -1,5 +1,5 @@ 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/source/collection_lens.dart'; import 'package:aves/model/source/enums/enums.dart'; diff --git a/lib/widgets/collection/query_bar.dart b/lib/widgets/collection/query_bar.dart index d1442b4c3..e9bc1cc68 100644 --- a/lib/widgets/collection/query_bar.dart +++ b/lib/widgets/collection/query_bar.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/source/collection_lens.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; diff --git a/lib/widgets/common/action_controls/quick_choosers/share_button.dart b/lib/widgets/common/action_controls/quick_choosers/share_button.dart index 379c6b9a2..7e0164400 100644 --- a/lib/widgets/common/action_controls/quick_choosers/share_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/share_button.dart @@ -1,6 +1,7 @@ import 'package:aves/model/actions/entry_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/share_chooser.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/action_controls/togglers/favourite.dart b/lib/widgets/common/action_controls/togglers/favourite.dart index 510853037..4589b528e 100644 --- a/lib/widgets/common/action_controls/togglers/favourite.dart +++ b/lib/widgets/common/action_controls/togglers/favourite.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/theme/colors.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/common/action_controls/togglers/mute.dart b/lib/widgets/common/action_controls/togglers/mute.dart index 7eec6faac..fa9a2a693 100644 --- a/lib/widgets/common/action_controls/togglers/mute.dart +++ b/lib/widgets/common/action_controls/togglers/mute.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/extensions/build_context.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'; class MuteToggler extends StatelessWidget { diff --git a/lib/widgets/common/action_controls/togglers/play.dart b/lib/widgets/common/action_controls/togglers/play.dart index e2f5bbe6b..7ca45ffe3 100644 --- a/lib/widgets/common/action_controls/togglers/play.dart +++ b/lib/widgets/common/action_controls/togglers/play.dart @@ -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/extensions/build_context.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:provider/provider.dart'; diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index 40471bbde..ff1ae2d70 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -1,5 +1,6 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_metadata_edition.dart'; +import 'package:aves/model/entry/entry.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/placeholder.dart'; import 'package:aves/model/filters/tag.dart'; diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index e3c1e5a12..768a2d085 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'package:aves/app_mode.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/trash.dart'; import 'package:aves/model/highlight.dart'; diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index db158802b..5d8fa13f9 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.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/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index cfcb39df0..d1a93321b 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:math'; 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/utils/android_file_utils.dart'; import 'package:aves/utils/collection_utils.dart'; diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 2e7bdd1ef..72aabace6 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -1,6 +1,9 @@ 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/settings.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index 20a3443d9..0428f0ca0 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -1,6 +1,8 @@ import 'package:aves/image_providers/app_icon_image_provider.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/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 0643b2ce4..e18bda838 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.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/settings.dart'; import 'package:aves/services/common/services.dart'; @@ -343,7 +345,7 @@ class _GeoMapState extends State { ZoomedBounds? _initBoundsForEntries({required List entries, int? recentCount}) { if (recentCount != null) { - entries = List.of(entries)..sort(AvesEntry.compareByDate); + entries = List.of(entries)..sort(AvesEntrySort.compareByDate); entries = entries.take(recentCount).toList(); } diff --git a/lib/widgets/common/thumbnail/decorated.dart b/lib/widgets/common/thumbnail/decorated.dart index 93ccf66ac..9240b1f12 100644 --- a/lib/widgets/common/thumbnail/decorated.dart +++ b/lib/widgets/common/thumbnail/decorated.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/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/grid/overlay.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; diff --git a/lib/widgets/common/thumbnail/error.dart b/lib/widgets/common/thumbnail/error.dart index bb6fa6000..0ad1eb85f 100644 --- a/lib/widgets/common/thumbnail/error.dart +++ b/lib/widgets/common/thumbnail/error.dart @@ -1,7 +1,7 @@ import 'dart:io'; 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/utils/mime_utils.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 9d7dd5c55..a639f475f 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -2,8 +2,9 @@ import 'dart:math'; import 'dart:ui'; import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.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/entry_background.dart'; import 'package:aves/model/settings/enums/enums.dart'; diff --git a/lib/widgets/common/thumbnail/notifications.dart b/lib/widgets/common/thumbnail/notifications.dart index b52226070..d5edca496 100644 --- a/lib/widgets/common/thumbnail/notifications.dart +++ b/lib/widgets/common/thumbnail/notifications.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:flutter/widgets.dart'; @immutable diff --git a/lib/widgets/common/thumbnail/overlay.dart b/lib/widgets/common/thumbnail/overlay.dart index ed64484d5..6326be3d4 100644 --- a/lib/widgets/common/thumbnail/overlay.dart +++ b/lib/widgets/common/thumbnail/overlay.dart @@ -1,6 +1,6 @@ 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/selection.dart'; import 'package:aves/theme/durations.dart'; diff --git a/lib/widgets/common/thumbnail/scroller.dart b/lib/widgets/common/thumbnail/scroller.dart index 25cf3d955..ab5680dd6 100644 --- a/lib/widgets/common/thumbnail/scroller.dart +++ b/lib/widgets/common/thumbnail/scroller.dart @@ -1,6 +1,6 @@ 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/widgets/common/behaviour/known_extent_scroll_physics.dart'; import 'package:aves/widgets/common/grid/theme.dart'; diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index dca8795fa..d5d2eb5cf 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -1,5 +1,5 @@ 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/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index f25fc4108..f445e3df6 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -1,5 +1,5 @@ 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/source/collection_lens.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/dialogs/convert_entry_dialog.dart b/lib/widgets/dialogs/convert_entry_dialog.dart index 6fe70a92e..090c1b0f4 100644 --- a/lib/widgets/dialogs/convert_entry_dialog.dart +++ b/lib/widgets/dialogs/convert_entry_dialog.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/length_unit.dart'; import 'package:aves/model/settings/settings.dart'; @@ -148,7 +149,7 @@ class _ConvertEntryDialogState extends State { ), ), const SizedBox(width: 8), - const Text(AvesEntry.resolutionSeparator), + const Text(ExtraAvesEntryProps.resolutionSeparator), const SizedBox(width: 8), Expanded( child: TextField( diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index ea1f51447..de368dc55 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -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/enums/date_edit_action.dart'; import 'package:aves/model/metadata/enums/date_field_source.dart'; diff --git a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart index 4478de495..e60e49cae 100644 --- a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_description_dialog.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/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index 1b81adf64..f5e6a8e34 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -1,5 +1,6 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_metadata_edition.dart'; +import 'package:aves/model/entry/entry.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/location_edit_action.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart'; diff --git a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart index e1dab151a..c118cbb8f 100644 --- a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.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/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart index 262b60474..ff1a175a8 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart @@ -1,6 +1,6 @@ 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/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart index 5980f0993..55d3f68d7 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart @@ -1,6 +1,6 @@ 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/settings/settings.dart'; import 'package:aves/theme/durations.dart'; diff --git a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart index fe4a06c52..87d5e66a1 100644 --- a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart +++ b/lib/widgets/dialogs/entry_editors/tag_editor_page.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/placeholder.dart'; import 'package:aves/model/filters/tag.dart'; diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index afbeb28f2..30aee7b04 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -1,7 +1,7 @@ import 'dart:math'; 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/filters.dart'; import 'package:aves/model/settings/enums/enums.dart'; diff --git a/lib/widgets/dialogs/item_picker.dart b/lib/widgets/dialogs/item_picker.dart index 2c5c2631d..1b908c1f0 100644 --- a/lib/widgets/dialogs/item_picker.dart +++ b/lib/widgets/dialogs/item_picker.dart @@ -1,6 +1,6 @@ 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/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; diff --git a/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart index 72ad90cc0..4631a2b94 100644 --- a/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart @@ -1,5 +1,5 @@ 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/source/collection_lens.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index eb0e65350..59787370f 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.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/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.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:flutter/material.dart'; @@ -117,7 +118,7 @@ class _VideoStreamSelectionDialogState extends State final w = stream.width; final h = stream.height; if (w != null && h != null) { - return '$common • $w${AvesEntry.resolutionSeparator}$h'; + return '$common • $w${ExtraAvesEntryProps.resolutionSeparator}$h'; } } return common; diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index b39500edd..5cd758ffb 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -1,4 +1,5 @@ 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/filters.dart'; import 'package:aves/model/settings/settings.dart'; diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 1565080f7..207802309 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.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/move_type.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/filters.dart'; import 'package:aves/model/highlight.dart'; diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index 50e7dde04..a9b9c20a2 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -1,7 +1,7 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_set_actions.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/filters.dart'; import 'package:aves/model/query.dart'; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 38d773180..35c249d8a 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/query.dart'; @@ -12,8 +13,8 @@ import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/double_back.dart'; diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index 84c11a2e3..d7cacb7d7 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.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/filters.dart'; import 'package:aves/model/source/collection_source.dart'; diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index e03454aaf..feb55bf88 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:aves/app_mode.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/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/enums/enums.dart'; diff --git a/lib/widgets/home_widget.dart b/lib/widgets/home_widget.dart index 95256f867..69f8c61b1 100644 --- a/lib/widgets/home_widget.dart +++ b/lib/widgets/home_widget.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/widget_shape.dart'; import 'package:aves/utils/constants.dart'; diff --git a/lib/widgets/map/map_info_row.dart b/lib/widgets/map/map_info_row.dart index 1bef21f85..67a600cbf 100644 --- a/lib/widgets/map/map_info_row.dart +++ b/lib/widgets/map/map_info_row.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/settings.dart'; import 'package:aves/services/common/services.dart'; diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index b3629257b..5cf00c41b 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/map_actions.dart'; import 'package:aves/model/actions/map_cluster_actions.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/coordinate.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/geotiff.dart'; @@ -31,8 +32,8 @@ import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/map/map_info_row.dart'; -import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; +import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves_map/aves_map.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/stats/date/histogram.dart b/lib/widgets/stats/date/histogram.dart index bf268a171..772c07310 100644 --- a/lib/widgets/stats/date/histogram.dart +++ b/lib/widgets/stats/date/histogram.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/sort.dart'; import 'package:aves/model/filters/date.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -50,7 +51,7 @@ class _HistogramState extends State with AutomaticKeepAliveClientMixi void initState() { super.initState(); - final entriesByDateDescending = List.of(widget.entries)..sort(AvesEntry.compareByDate); + final entriesByDateDescending = List.of(widget.entries)..sort(AvesEntrySort.compareByDate); var lastDate = entriesByDateDescending.firstWhereOrNull((entry) => entry.bestDate != null)?.bestDate; var firstDate = entriesByDateDescending.lastWhereOrNull((entry) => entry.bestDate != null)?.bestDate; diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index ac28daf8d..1a6ff3ff6 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -1,6 +1,6 @@ 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/filters.dart'; import 'package:aves/model/filters/location.dart'; diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 094035435..ee5ed6e72 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -6,8 +6,12 @@ import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/device.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_metadata_edition.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/favourites.dart'; +import 'package:aves/model/entry/extensions/location.dart'; +import 'package:aves/model/entry/extensions/metadata_edition.dart'; +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/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; @@ -243,7 +247,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.openVideo: final controller = context.read().getController(targetEntry); if (controller != null) { - VideoActionNotification(controller: controller, action: action).dispatch(context); + VideoActionNotification( + controller: controller, + entry: targetEntry, + action: action, + ).dispatch(context); } break; case EntryAction.edit: diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 469252264..62eeffbd0 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -4,9 +4,11 @@ import 'dart:convert'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/events.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_info.dart'; -import 'package:aves/model/entry_metadata_edition.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/info.dart'; +import 'package:aves/model/entry/extensions/metadata_edition.dart'; +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/geotiff.dart'; import 'package:aves/model/settings/settings.dart'; diff --git a/lib/widgets/viewer/action/printer.dart b/lib/widgets/viewer/action/printer.dart index 27f8a80ac..d2969c953 100644 --- a/lib/widgets/viewer/action/printer.dart +++ b/lib/widgets/viewer/action/printer.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; +import 'package:aves/model/entry/extensions/multipage.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/viewer/action/single_entry_editor.dart b/lib/widgets/viewer/action/single_entry_editor.dart index 3bdf65348..bee999ece 100644 --- a/lib/widgets/viewer/action/single_entry_editor.dart +++ b/lib/widgets/viewer/action/single_entry_editor.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:aves/app_mode.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/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/services.dart'; diff --git a/lib/widgets/viewer/action/video_action_delegate.dart b/lib/widgets/viewer/action/video_action_delegate.dart index e9562998a..81d91e871 100644 --- a/lib/widgets/viewer/action/video_action_delegate.dart +++ b/lib/widgets/viewer/action/video_action_delegate.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/location.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; @@ -18,7 +21,7 @@ import 'package:aves/widgets/dialogs/video_speed_dialog.dart'; import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart'; import 'package:aves/widgets/settings/video/video_settings_page.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -35,14 +38,14 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix stopOverlayHidingTimer(); } - Future onActionSelected(BuildContext context, AvesVideoController controller, EntryAction action) async { + Future onActionSelected(BuildContext context, AvesEntry entry, AvesVideoController controller, EntryAction action) async { // make sure overlay is not disappearing when selecting an action stopOverlayHidingTimer(); const ToggleOverlayNotification(visible: true).dispatch(context); switch (action) { case EntryAction.videoCaptureFrame: - await _captureFrame(context, controller); + await _captureFrame(context, entry, controller); break; case EntryAction.videoToggleMute: await controller.mute(!controller.isMuted); @@ -66,7 +69,6 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix await controller.seekTo(controller.currentPosition + 10000); break; case EntryAction.openVideo: - final entry = controller.entry; await androidAppService.open(entry.uri, entry.mimeTypeAnySubtype, forceChooser: false).then((success) { if (!success) showNoMatchingAppDialog(context); }); @@ -76,7 +78,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } - Future _captureFrame(BuildContext context, AvesVideoController controller) async { + Future _captureFrame(BuildContext context, AvesEntry entry, AvesVideoController controller) async { final positionMillis = controller.currentPosition; final bytes = await controller.captureFrame(); @@ -85,7 +87,6 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await checkFreeSpace(context, bytes.length, destinationAlbum)) return; - final entry = controller.entry; final rotationDegrees = entry.rotationDegrees; final dateTimeMillis = entry.catalogMetadata?.dateMillis; final latLng = entry.latLng; diff --git a/lib/widgets/viewer/controls/controller.dart b/lib/widgets/viewer/controls/controller.dart index 3ee3f2a6e..5808a11ee 100644 --- a/lib/widgets/viewer/controls/controller.dart +++ b/lib/widgets/viewer/controls/controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/viewer/controls/events.dart'; diff --git a/lib/widgets/viewer/controls/notifications.dart b/lib/widgets/viewer/controls/notifications.dart index 74850f6dd..5cb76faa7 100644 --- a/lib/widgets/viewer/controls/notifications.dart +++ b/lib/widgets/viewer/controls/notifications.dart @@ -1,8 +1,8 @@ import 'package:aves/model/actions/entry_actions.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/filters/filters.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; @@ -62,10 +62,12 @@ class TvShowMoreInfoNotification extends Notification {} @immutable class VideoActionNotification extends Notification { final AvesVideoController controller; + final AvesEntry entry; final EntryAction action; const VideoActionNotification({ required this.controller, + required this.entry, required this.action, }); } diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index a7a039e8e..fded8c92d 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/trash.dart'; diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart index 28b22af83..a45b213ed 100644 --- a/lib/widgets/viewer/debug/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -1,6 +1,10 @@ import 'package:aves/app_mode.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/favourites.dart'; +import 'package:aves/model/entry/extensions/location.dart'; +import 'package:aves/model/entry/extensions/images.dart'; +import 'package:aves/model/entry/extensions/multipage.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/viewer/debug/db.dart'; import 'package:aves/widgets/viewer/debug/metadata.dart'; diff --git a/lib/widgets/viewer/debug/metadata.dart b/lib/widgets/viewer/debug/metadata.dart index 0d6e47327..99e403ccc 100644 --- a/lib/widgets/viewer/debug/metadata.dart +++ b/lib/widgets/viewer/debug/metadata.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'dart:typed_data'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/android_debug_service.dart'; import 'package:aves/utils/constants.dart'; diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index 82bad368e..c491edad4 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.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/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/viewer_transition.dart'; import 'package:aves/model/settings/settings.dart'; diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 3f7c7a0d2..37fd1c6f5 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -4,7 +4,10 @@ import 'dart:ui'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.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/extensions/props.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; @@ -240,7 +243,7 @@ class _ViewerVerticalPageViewState extends State { ShowInfoIntent: CallbackAction(onInvoke: (intent) => ShowInfoPageNotification().dispatch(context)), TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)), TvShowMoreInfoIntent: CallbackAction(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)), - PlayPauseIntent: CallbackAction(onInvoke: (intent) => _onPlayPauseIntent(intent, entry)), + PlayPauseIntent: CallbackAction(onInvoke: (intent) => _onPlayPauseIntent(intent)), EntryActionIntent: CallbackAction(onInvoke: (intent) => _onEntryActionIntent(intent.action)), ActivateIntent: CallbackAction(onInvoke: (intent) { if (useTvLayout) { @@ -249,7 +252,11 @@ class _ViewerVerticalPageViewState extends State { // address `TV-PC` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality final controller = context.read().getController(_entry); if (controller != null) { - VideoActionNotification(controller: controller, action: EntryAction.videoTogglePlay).dispatch(context); + VideoActionNotification( + controller: controller, + entry: _entry, + action: EntryAction.videoTogglePlay, + ).dispatch(context); } } else { const ToggleOverlayNotification().dispatch(context); @@ -357,7 +364,7 @@ class _ViewerVerticalPageViewState extends State { } } - void _onPlayPauseIntent(PlayPauseIntent intent, entry) { + void _onPlayPauseIntent(PlayPauseIntent intent) { // address `TV-PP` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality final _entry = entry; if (_entry != null && _entry.isVideo) { @@ -376,7 +383,11 @@ class _ViewerVerticalPageViewState extends State { break; } if (toggle) { - VideoActionNotification(controller: controller, action: EntryAction.videoTogglePlay).dispatch(context); + VideoActionNotification( + controller: controller, + entry: _entry, + action: EntryAction.videoTogglePlay, + ).dispatch(context); } } } diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 527da25df..94ef5b5d7 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/viewer/controls/controller.dart'; diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 45dda3132..876ef773b 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -4,7 +4,9 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/move_type.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/extensions/multipage.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/highlight.dart'; @@ -31,7 +33,7 @@ import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves/widgets/viewer/overlay/video/video.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart'; import 'package:aves/widgets/viewer/visual/controller_mixin.dart'; import 'package:collection/collection.dart'; @@ -373,7 +375,12 @@ class _EntryViewerStackState extends State with EntryViewContr scale: _overlayVideoControlScale, onActionSelected: (action) { if (videoController != null) { - _onVideoAction(context, videoController, action); + _onVideoAction( + context: context, + entry: targetEntry, + controller: videoController, + action: action, + ); } }, onActionMenuOpened: () { @@ -484,9 +491,12 @@ class _EntryViewerStackState extends State with EntryViewContr } else if (notification is ToggleOverlayNotification) { _overlayVisible.value = notification.visible ?? !_overlayVisible.value; } else if (notification is VideoActionNotification) { - final controller = notification.controller; - final action = notification.action; - _onVideoAction(context, controller, action); + _onVideoAction( + context: context, + entry: notification.entry, + controller: notification.controller, + action: notification.action, + ); } else if (notification is TvShowLessInfoNotification) { if (_overlayVisible.value) { _overlayVisible.value = false; @@ -513,8 +523,13 @@ class _EntryViewerStackState extends State with EntryViewContr return true; } - Future _onVideoAction(BuildContext context, AvesVideoController controller, EntryAction action) async { - await _videoActionDelegate.onActionSelected(context, controller, action); + Future _onVideoAction({ + required BuildContext context, + required AvesEntry entry, + required AvesVideoController controller, + required EntryAction action, + }) async { + await _videoActionDelegate.onActionSelected(context, entry, controller, action); if (action == EntryAction.videoToggleMute) { final override = controller.isMuted; videoMutedOverride = override; @@ -747,8 +762,7 @@ class _EntryViewerStackState extends State with EntryViewContr Future _enablePictureInPicture() async { final videoController = context.read().getPlayingController(); if (videoController != null) { - final targetEntry = videoController.entry; - final entrySize = targetEntry.displaySize; + final entrySize = videoController.entry.displaySize; final aspectRatio = Rational(entrySize.width.round(), entrySize.height.round()); final mq = context.read(); diff --git a/lib/widgets/viewer/hero.dart b/lib/widgets/viewer/hero.dart index 6c828d519..3e9c19f9f 100644 --- a/lib/widgets/viewer/hero.dart +++ b/lib/widgets/viewer/hero.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:flutter/widgets.dart'; diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 4f081f3d2..b1da03c60 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -1,7 +1,10 @@ import 'package:aves/app_mode.dart'; import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/actions/entry_actions.dart'; -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/favourites.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; diff --git a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart index d9f33d63d..ff96ebdcd 100644 --- a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 4e05c0c86..689152407 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,6 +1,7 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; -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/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 26c4dfeb7..3820af71c 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/events.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/filters/filters.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/source/collection_lens.dart'; diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 008fa535e..5f5d48307 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.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/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 208d270fe..158e44a6e 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -1,5 +1,6 @@ import 'package:aves/app_mode.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/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/settings.dart'; diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index dc7a8f6f3..c353eb785 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/theme/colors.dart'; diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 80e76a7ce..8c4777cb2 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_info.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/info.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 542b001eb..77d0407fc 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/common/services.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/info/metadata/tv_page.dart b/lib/widgets/viewer/info/metadata/tv_page.dart index dbf3033f4..46af8c63b 100644 --- a/lib/widgets/viewer/info/metadata/tv_page.dart +++ b/lib/widgets/viewer/info/metadata/tv_page.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/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/intents.dart'; diff --git a/lib/widgets/viewer/multipage/conductor.dart b/lib/widgets/viewer/multipage/conductor.dart index da6915149..bac45d9f2 100644 --- a/lib/widgets/viewer/multipage/conductor.dart +++ b/lib/widgets/viewer/multipage/conductor.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widgets/viewer/multipage/controller.dart b/lib/widgets/viewer/multipage/controller.dart index 5a7c68cd6..a2332915f 100644 --- a/lib/widgets/viewer/multipage/controller.dart +++ b/lib/widgets/viewer/multipage/controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; -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/multipage.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 2c3f15efb..d80348978 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -1,7 +1,8 @@ import 'dart:math'; import 'package:aves/app_mode.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/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; diff --git a/lib/widgets/viewer/overlay/details/date.dart b/lib/widgets/viewer/overlay/details/date.dart index da3bdb092..67e24fadd 100644 --- a/lib/widgets/viewer/overlay/details/date.dart +++ b/lib/widgets/viewer/overlay/details/date.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/theme/format.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; diff --git a/lib/widgets/viewer/overlay/details/details.dart b/lib/widgets/viewer/overlay/details/details.dart index d24265ee7..3b10442c1 100644 --- a/lib/widgets/viewer/overlay/details/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; diff --git a/lib/widgets/viewer/overlay/details/location.dart b/lib/widgets/viewer/overlay/details/location.dart index aecf9dd2a..ad3ebd4af 100644 --- a/lib/widgets/viewer/overlay/details/location.dart +++ b/lib/widgets/viewer/overlay/details/location.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/settings.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/viewer/overlay/details/position_title.dart b/lib/widgets/viewer/overlay/details/position_title.dart index 9d4767925..d13640e95 100644 --- a/lib/widgets/viewer/overlay/details/position_title.dart +++ b/lib/widgets/viewer/overlay/details/position_title.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; diff --git a/lib/widgets/viewer/overlay/details/rating_tags.dart b/lib/widgets/viewer/overlay/details/rating_tags.dart index f99c222a1..9a837f478 100644 --- a/lib/widgets/viewer/overlay/details/rating_tags.dart +++ b/lib/widgets/viewer/overlay/details/rating_tags.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/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; diff --git a/lib/widgets/viewer/overlay/panorama.dart b/lib/widgets/viewer/overlay/panorama.dart index 5f853f583..2f58aae82 100644 --- a/lib/widgets/viewer/overlay/panorama.dart +++ b/lib/widgets/viewer/overlay/panorama.dart @@ -1,6 +1,6 @@ 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/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; diff --git a/lib/widgets/viewer/overlay/selection_button.dart b/lib/widgets/viewer/overlay/selection_button.dart index ad2f1b899..874492854 100644 --- a/lib/widgets/viewer/overlay/selection_button.dart +++ b/lib/widgets/viewer/overlay/selection_button.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/theme/durations.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/viewer/overlay/thumbnail_preview.dart b/lib/widgets/viewer/overlay/thumbnail_preview.dart index a7fa1119a..934ad7518 100644 --- a/lib/widgets/viewer/overlay/thumbnail_preview.dart +++ b/lib/widgets/viewer/overlay/thumbnail_preview.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 7021e0149..67cd07c31 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.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/model/settings/settings.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; diff --git a/lib/widgets/viewer/overlay/video/controls.dart b/lib/widgets/viewer/overlay/video/controls.dart index 71db0d998..d2522896f 100644 --- a/lib/widgets/viewer/overlay/video/controls.dart +++ b/lib/widgets/viewer/overlay/video/controls.dart @@ -1,13 +1,15 @@ import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/action_controls/togglers/play.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class VideoControlRow extends StatelessWidget { + final AvesEntry entry; final AvesVideoController? controller; final Animation scale; final Function(EntryAction value) onActionSelected; @@ -17,6 +19,7 @@ class VideoControlRow extends StatelessWidget { const VideoControlRow({ super.key, + required this.entry, required this.controller, required this.scale, required this.onActionSelected, @@ -63,10 +66,9 @@ class VideoControlRow extends StatelessWidget { ], ); case VideoControls.playOutside: - final trashed = controller?.entry.trashed ?? false; return Padding( padding: const EdgeInsetsDirectional.only(start: padding), - child: _buildIconButton(context, EntryAction.openVideo, enabled: !trashed), + child: _buildIconButton(context, EntryAction.openVideo, enabled: !entry.trashed), ); case VideoControls.none: return const SizedBox(); diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index b96e26c10..aac65fe25 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -7,7 +7,7 @@ import 'package:aves/theme/themes.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/viewer/overlay/video/video.dart b/lib/widgets/viewer/overlay/video/video.dart index fa82c7fc1..409ea7bd2 100644 --- a/lib/widgets/viewer/overlay/video/video.dart +++ b/lib/widgets/viewer/overlay/video/video.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/overlay/video/controls.dart'; import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; class VideoControlOverlay extends StatefulWidget { @@ -69,6 +69,7 @@ class _VideoControlOverlayState extends State with SingleTi ), ), VideoControlRow( + entry: entry, controller: controller, scale: scale, onActionSelected: widget.onActionSelected, diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index faf3196da..52a27e5db 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -2,7 +2,9 @@ import 'dart:math'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_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/model/entry/extensions/props.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; @@ -24,7 +26,7 @@ import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/action/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index ba894d6e1..3d949f033 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -2,8 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/wallpaper_target.dart'; import 'package:aves/services/wallpaper_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; diff --git a/lib/widgets/viewer/page_entry_builder.dart b/lib/widgets/viewer/page_entry_builder.dart index c3b25aca0..97fcb5db0 100644 --- a/lib/widgets/viewer/page_entry_builder.dart +++ b/lib/widgets/viewer/page_entry_builder.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index ea1dbcd8f..00a97cb45 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; diff --git a/lib/widgets/viewer/slideshow_page.dart b/lib/widgets/viewer/slideshow_page.dart index c4762de49..48bcd0e2e 100644 --- a/lib/widgets/viewer/slideshow_page.dart +++ b/lib/widgets/viewer/slideshow_page.dart @@ -1,6 +1,6 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/slideshow_actions.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/mime.dart'; import 'package:aves/model/settings/enums/enums.dart'; diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index 82abe32c3..7178949a4 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -1,11 +1,13 @@ import 'dart:async'; -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/settings.dart'; import 'package:aves/model/source/collection_lens.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:aves/widgets/viewer/video/db_playback_state_handler.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart'; import 'package:collection/collection.dart'; @@ -13,6 +15,7 @@ class VideoConductor { final CollectionLens? _collection; final List _controllers = []; final List _subscriptions = []; + final PlaybackStateHandler playbackStateHandler = DatabasePlaybackStateHandler(); static const _defaultMaxControllerCount = 3; @@ -34,8 +37,8 @@ class VideoConductor { if (controller != null) { _controllers.remove(controller); } else { - controller = IjkPlayerAvesVideoController(entry, persistPlayback: true); - _subscriptions.add(controller.statusStream.listen((event) => _onControllerStatusChanged(controller!, event))); + controller = IjkPlayerAvesVideoController(entry, playbackStateHandler: playbackStateHandler); + _subscriptions.add(controller.statusStream.listen((event) => _onControllerStatusChanged(entry, controller!, event))); } _controllers.insert(0, controller); while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) { @@ -50,11 +53,11 @@ class VideoConductor { return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); } - Future _onControllerStatusChanged(AvesVideoController controller, VideoStatus status) async { + Future _onControllerStatusChanged(AvesEntry entry, AvesVideoController controller, VideoStatus status) async { bool canSkipToNext = false, canSkipToPrevious = false; final entries = _collection?.sortedEntries; if (entries != null) { - final currentIndex = entries.indexOf(controller.entry); + final currentIndex = entries.indexOf(entry); if (currentIndex != -1) { bool isVideo(AvesEntry entry) => entry.isVideo; canSkipToPrevious = entries.take(currentIndex).lastWhereOrNull(isVideo) != null; @@ -63,6 +66,7 @@ class VideoConductor { } await mediaSessionService.update( + entry: entry, controller: controller, canSkipToNext: canSkipToNext, canSkipToPrevious: canSkipToPrevious, diff --git a/lib/widgets/viewer/video/db_playback_state_handler.dart b/lib/widgets/viewer/video/db_playback_state_handler.dart new file mode 100644 index 000000000..675e25e1f --- /dev/null +++ b/lib/widgets/viewer/video/db_playback_state_handler.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +import 'package:aves/model/video_playback.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/format.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_video/aves_video.dart'; +import 'package:flutter/material.dart'; + +class DatabasePlaybackStateHandler extends PlaybackStateHandler { + static const resumeTimeSaveMinProgress = .05; + static const resumeTimeSaveMaxProgress = .95; + + @override + Future getResumeTime({required int entryId, required BuildContext context}) async { + final playback = await metadataDb.loadVideoPlayback(entryId); + final resumeTime = playback?.resumeTimeMillis ?? 0; + if (resumeTime == 0) return null; + + // clear on retrieval + await metadataDb.removeVideoPlayback({entryId}); + + final resume = await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), + actions: [ + TextButton( + onPressed: () => Navigator.maybeOf(context)?.pop(false), + child: Text(context.l10n.videoStartOverButtonLabel), + ), + TextButton( + onPressed: () => Navigator.maybeOf(context)?.pop(true), + child: Text(context.l10n.videoResumeButtonLabel), + ), + ], + ), + routeSettings: const RouteSettings(name: AvesDialog.confirmationRouteName), + ); + if (resume == null || !resume) return 0; + return resumeTime; + } + + @override + Future saveResumeTime({required int entryId, required int position, required double progress}) async { + if (resumeTimeSaveMinProgress < progress && progress < resumeTimeSaveMaxProgress) { + await metadataDb.addVideoPlayback({ + VideoPlaybackRow( + entryId: entryId, + resumeTimeMillis: position, + ) + }); + } else { + await metadataDb.removeVideoPlayback({entryId}); + } + } +} diff --git a/lib/widgets/viewer/video/fijkplayer.dart b/lib/widgets/viewer/video/fijkplayer.dart index 9b7d10a6f..2237ab41b 100644 --- a/lib/widgets/viewer/video/fijkplayer.dart +++ b/lib/widgets/viewer/video/fijkplayer.dart @@ -1,13 +1,13 @@ import 'dart:async'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/video_loop_mode.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/video/keys.dart'; import 'package:aves/model/video/metadata.dart'; import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves/utils/change_notifier.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/material.dart'; @@ -62,12 +62,9 @@ class IjkPlayerAvesVideoController extends AvesVideoController { static const captureFrameEnabled = true; IjkPlayerAvesVideoController( - AvesEntry entry, { - required bool persistPlayback, - }) : super( - entry, - persistPlayback: persistPlayback, - ) { + super.entry, { + required super.playbackStateHandler, + }) { _instance = FijkPlayer(); _valueStream.map((value) => value.videoRenderStart).firstWhere((v) => v, orElse: () => false).then( (started) { @@ -168,7 +165,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { _macroBlockCrop = Offset(s.width, s.height); } - final loopEnabled = settings.videoLoopMode.shouldLoop(entry); + final loopEnabled = settings.videoLoopMode.shouldLoop(entry.durationMillis); // `fastseek`: enable fast, but inaccurate seeks for some formats // in practice the flag seems ineffective, but harmless too diff --git a/lib/widgets/viewer/video/flutter_vlc_player.dart b/lib/widgets/viewer/video/flutter_vlc_player.dart index d8f4dcaf4..df28f0ffc 100644 --- a/lib/widgets/viewer/video/flutter_vlc_player.dart +++ b/lib/widgets/viewer/video/flutter_vlc_player.dart @@ -3,7 +3,7 @@ // // import 'package:aves/model/entry.dart'; // import 'package:aves/utils/change_notifier.dart'; -// import 'package:aves/widgets/viewer/video/controller.dart'; +// import 'package:aves_video/aves_video.dart'; // import 'package:flutter/material.dart'; // import 'package:flutter/src/foundation/change_notifier.dart'; // import 'package:flutter/src/widgets/framework.dart'; diff --git a/lib/widgets/viewer/video/video_player.dart b/lib/widgets/viewer/video/video_player.dart index ee6d438c7..b3b4a663c 100644 --- a/lib/widgets/viewer/video/video_player.dart +++ b/lib/widgets/viewer/video/video_player.dart @@ -2,7 +2,7 @@ // // import 'package:aves/model/entry.dart'; // import 'package:aves/utils/change_notifier.dart'; -// import 'package:aves/widgets/viewer/video/controller.dart'; +// import 'package:aves_video/aves_video.dart'; // import 'package:flutter/src/foundation/change_notifier.dart'; // import 'package:flutter/src/widgets/framework.dart'; // import 'package:video_player/video_player.dart'; diff --git a/lib/widgets/viewer/visual/conductor.dart b/lib/widgets/viewer/visual/conductor.dart index d350c34ae..c6801c6a1 100644 --- a/lib/widgets/viewer/visual/conductor.dart +++ b/lib/widgets/viewer/visual/conductor.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/widgets/viewer/visual/state.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widgets/viewer/visual/controller_mixin.dart b/lib/widgets/viewer/visual/controller_mixin.dart index f955fe8a2..6f39dd025 100644 --- a/lib/widgets/viewer/visual/controller_mixin.dart +++ b/lib/widgets/viewer/visual/controller_mixin.dart @@ -1,12 +1,14 @@ import 'package:aves/app_mode.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/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 91af37de0..fff2d55d1 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; -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/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; @@ -233,6 +234,7 @@ class _EntryPageViewState extends State with SingleTickerProvider ); VideoActionNotification( controller: videoController, + entry: entry, action: action, ).dispatch(context); } @@ -326,6 +328,7 @@ class _EntryPageViewState extends State with SingleTickerProvider ), ), VideoSubtitles( + entry: entry, controller: videoController, viewStateNotifier: _viewStateNotifier, ), diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index 85ce3d3d9..50d8167ee 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index a7770966e..1924c3fad 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -1,8 +1,9 @@ import 'dart:math'; import 'package:aves/image_providers/region_provider.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/entry_background.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 02736fa30..b7eb3809c 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -2,8 +2,9 @@ import 'dart:math'; import 'dart:ui'; import 'package:aves/image_providers/region_provider.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/entry_background.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; diff --git a/lib/widgets/viewer/visual/video/cover.dart b/lib/widgets/viewer/visual/video/cover.dart index e9a3a81e1..6091cbd69 100644 --- a/lib/widgets/viewer/visual/video/cover.dart +++ b/lib/widgets/viewer/visual/video/cover.dart @@ -1,8 +1,9 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/images.dart'; +import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/visual/video/subtitle/subtitle.dart b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart index c83f9db6f..48919273c 100644 --- a/lib/widgets/viewer/visual/video/subtitle/subtitle.dart +++ b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart @@ -1,8 +1,10 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/subtitle_position.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/basic/text/background_painter.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/ass_parser.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart'; @@ -14,6 +16,7 @@ import 'package:latlong2/latlong.dart' as angles; import 'package:provider/provider.dart'; class VideoSubtitles extends StatelessWidget { + final AvesEntry entry; final AvesVideoController controller; final ValueNotifier viewStateNotifier; final bool debugMode; @@ -22,6 +25,7 @@ class VideoSubtitles extends StatelessWidget { const VideoSubtitles({ super.key, + required this.entry, required this.controller, required this.viewStateNotifier, this.debugMode = false, @@ -29,7 +33,7 @@ class VideoSubtitles extends StatelessWidget { @override Widget build(BuildContext context) { - final videoDisplaySize = controller.entry.videoDisplaySize(controller.sarNotifier.value); + final videoDisplaySize = entry.videoDisplaySize(controller.sarNotifier.value); return IgnorePointer( child: Consumer( builder: (context, settings, child) { diff --git a/lib/widgets/viewer/visual/video/video_view.dart b/lib/widgets/viewer/visual/video/video_view.dart index 6fadf9908..06c3d6819 100644 --- a/lib/widgets/viewer/visual/video/video_view.dart +++ b/lib/widgets/viewer/visual/video/video_view.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; class VideoView extends StatefulWidget { diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index 5d6ad023a..7d398f5d3 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -1,5 +1,7 @@ import 'package:aves/model/actions/entry_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/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; @@ -17,9 +19,9 @@ import 'package:aves/widgets/viewer/overlay/video/video.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/providers.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; -import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/visual/controller_mixin.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:screen_brightness/screen_brightness.dart'; @@ -132,9 +134,12 @@ class _EntryEditorState extends State with EntryViewControllerMixin if (notification is ToggleOverlayNotification) { _overlayVisible.value = notification.visible ?? !_overlayVisible.value; } else if (notification is VideoActionNotification) { - final controller = notification.controller; - final action = notification.action; - _onVideoAction(context, controller, action); + _onVideoAction( + context: context, + entry: notification.entry, + controller: notification.controller, + action: notification.action, + ); } return true; }, @@ -176,7 +181,12 @@ class _EntryEditorState extends State with EntryViewControllerMixin entry: targetEntry, controller: videoController, scale: _overlayVideoControlScale, - onActionSelected: (action) => _onVideoAction(context, videoController, action), + onActionSelected: (action) => _onVideoAction( + context: context, + entry: targetEntry, + controller: videoController, + action: action, + ), onActionMenuOpened: () { // if the menu is opened while overlay is hiding, // the popup menu button is disposed and menu items are ineffective, @@ -236,9 +246,14 @@ class _EntryEditorState extends State with EntryViewControllerMixin ); } - void _onVideoAction(BuildContext context, AvesVideoController? videoController, EntryAction action) { - if (videoController != null) { - _videoActionDelegate.onActionSelected(context, videoController, action); + void _onVideoAction({ + required BuildContext context, + required AvesEntry entry, + required AvesVideoController? controller, + required EntryAction action, + }) { + if (controller != null) { + _videoActionDelegate.onActionSelected(context, entry, controller, action); } } diff --git a/plugins/aves_model/.gitignore b/plugins/aves_model/.gitignore new file mode 100644 index 000000000..28124a571 --- /dev/null +++ b/plugins/aves_model/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +#/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_model/.metadata b/plugins/aves_model/.metadata new file mode 100644 index 000000000..ad910f56c --- /dev/null +++ b/plugins/aves_model/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + channel: stable + +project_type: package diff --git a/plugins/aves_model/analysis_options.yaml b/plugins/aves_model/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/plugins/aves_model/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/plugins/aves_model/lib/aves_model.dart b/plugins/aves_model/lib/aves_model.dart new file mode 100644 index 000000000..4f604ce32 --- /dev/null +++ b/plugins/aves_model/lib/aves_model.dart @@ -0,0 +1,3 @@ +library aves_model; + +export 'src/entry_base.dart'; diff --git a/plugins/aves_model/lib/src/entry_base.dart b/plugins/aves_model/lib/src/entry_base.dart new file mode 100644 index 000000000..01966327d --- /dev/null +++ b/plugins/aves_model/lib/src/entry_base.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +mixin AvesEntryBase { + int get id; + + String get uri; + + int? get pageId; + + int? get sizeBytes; + + int? get durationMillis; + + int get rotationDegrees; + + Size get displaySize; + + double get displayAspectRatio; + + Listenable get visualChangeNotifier; +} diff --git a/plugins/aves_model/pubspec.lock b/plugins/aves_model/pubspec.lock new file mode 100644 index 000000000..6e38c33fc --- /dev/null +++ b/plugins/aves_model/pubspec.lock @@ -0,0 +1,79 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + characters: + dependency: transitive + description: + name: characters + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" + source: hosted + version: "1.17.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" +sdks: + dart: ">=2.19.4 <3.0.0" diff --git a/plugins/aves_model/pubspec.yaml b/plugins/aves_model/pubspec.yaml new file mode 100644 index 000000000..440c3befc --- /dev/null +++ b/plugins/aves_model/pubspec.yaml @@ -0,0 +1,15 @@ +name: aves_model +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=2.19.4 <3.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/plugins/aves_video/.gitignore b/plugins/aves_video/.gitignore new file mode 100644 index 000000000..28124a571 --- /dev/null +++ b/plugins/aves_video/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +#/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_video/.metadata b/plugins/aves_video/.metadata new file mode 100644 index 000000000..ad910f56c --- /dev/null +++ b/plugins/aves_video/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + channel: stable + +project_type: package diff --git a/plugins/aves_video/analysis_options.yaml b/plugins/aves_video/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_video/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_video/lib/aves_video.dart b/plugins/aves_video/lib/aves_video.dart new file mode 100644 index 000000000..90e236285 --- /dev/null +++ b/plugins/aves_video/lib/aves_video.dart @@ -0,0 +1,3 @@ +library aves_video; + +export 'src/controller.dart'; diff --git a/lib/widgets/viewer/video/controller.dart b/plugins/aves_video/lib/src/controller.dart similarity index 52% rename from lib/widgets/viewer/video/controller.dart rename to plugins/aves_video/lib/src/controller.dart index a41fb6d01..94330e789 100644 --- a/lib/widgets/viewer/video/controller.dart +++ b/plugins/aves_video/lib/src/controller.dart @@ -1,84 +1,33 @@ import 'dart:async'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/video_playback.dart'; -import 'package:aves/services/common/services.dart'; -import 'package:aves/theme/format.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; abstract class AvesVideoController { - final AvesEntry _entry; - final bool persistPlayback; + final AvesEntryBase _entry; + final PlaybackStateHandler playbackStateHandler; - AvesEntry get entry => _entry; + AvesEntryBase get entry => _entry; - static const resumeTimeSaveMinProgress = .05; - static const resumeTimeSaveMaxProgress = .95; static const resumeTimeSaveMinDuration = Duration(minutes: 2); - AvesVideoController(AvesEntry entry, {required this.persistPlayback}) : _entry = entry { + AvesVideoController(AvesEntryBase entry, {required this.playbackStateHandler}) : _entry = entry { entry.visualChangeNotifier.addListener(onVisualChanged); } @mustCallSuper Future dispose() async { - entry.visualChangeNotifier.removeListener(onVisualChanged); + _entry.visualChangeNotifier.removeListener(onVisualChanged); await _savePlaybackState(); } Future _savePlaybackState() async { - final id = entry.id; if (!isReady || duration < resumeTimeSaveMinDuration.inMilliseconds) return; - - if (persistPlayback) { - final _progress = progress; - if (resumeTimeSaveMinProgress < _progress && _progress < resumeTimeSaveMaxProgress) { - await metadataDb.addVideoPlayback({ - VideoPlaybackRow( - entryId: id, - resumeTimeMillis: currentPosition, - ) - }); - } else { - await metadataDb.removeVideoPlayback({id}); - } - } + await playbackStateHandler.saveResumeTime(entryId: _entry.id, position: currentPosition, progress: progress); } - Future getResumeTime(BuildContext context) async { - if (!persistPlayback) return null; - - final id = entry.id; - final playback = await metadataDb.loadVideoPlayback(id); - final resumeTime = playback?.resumeTimeMillis ?? 0; - if (resumeTime == 0) return null; - - // clear on retrieval - await metadataDb.removeVideoPlayback({id}); - - final resume = await showDialog( - context: context, - builder: (context) => AvesDialog( - content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), - actions: [ - TextButton( - onPressed: () => Navigator.maybeOf(context)?.pop(false), - child: Text(context.l10n.videoStartOverButtonLabel), - ), - TextButton( - onPressed: () => Navigator.maybeOf(context)?.pop(true), - child: Text(context.l10n.videoResumeButtonLabel), - ), - ], - ), - routeSettings: const RouteSettings(name: AvesDialog.confirmationRouteName), - ); - if (resume == null || !resume) return 0; - return resumeTime; - } + Future getResumeTime(BuildContext context) => playbackStateHandler.getResumeTime(entryId: _entry.id, context: context); void onVisualChanged(); @@ -179,3 +128,9 @@ class StreamSummary { @override String toString() => '$runtimeType#${shortHash(this)}{type: type, index: $index, codecName: $codecName, language: $language, title: $title, width: $width, height: $height}'; } + +abstract class PlaybackStateHandler { + Future getResumeTime({required int entryId, required BuildContext context}); + + Future saveResumeTime({required int entryId, required int position, required double progress}); +} diff --git a/plugins/aves_video/pubspec.lock b/plugins/aves_video/pubspec.lock new file mode 100644 index 000000000..1e86a4ad9 --- /dev/null +++ b/plugins/aves_video/pubspec.lock @@ -0,0 +1,94 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + aves_model: + dependency: "direct main" + description: + path: "../aves_model" + relative: true + source: path + version: "0.0.1" + characters: + dependency: transitive + description: + name: characters + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" + source: hosted + version: "1.17.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" +sdks: + dart: ">=2.19.4 <3.0.0" diff --git a/plugins/aves_video/pubspec.yaml b/plugins/aves_video/pubspec.yaml new file mode 100644 index 000000000..c4362c548 --- /dev/null +++ b/plugins/aves_video/pubspec.yaml @@ -0,0 +1,17 @@ +name: aves_video +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=2.19.4 <3.0.0' + +dependencies: + flutter: + sdk: flutter + aves_model: + path: ../aves_model + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/pubspec.lock b/pubspec.lock index 614344212..c894bb047 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -63,6 +63,13 @@ packages: relative: true source: path version: "0.0.1" + aves_model: + dependency: "direct main" + description: + path: "plugins/aves_model" + relative: true + source: path + version: "0.0.1" aves_report: dependency: "direct main" description: @@ -98,6 +105,13 @@ packages: relative: true source: path version: "0.0.1" + aves_video: + dependency: "direct main" + description: + path: "plugins/aves_video" + relative: true + source: path + version: "0.0.1" barcode: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c091321ea..74b0dc351 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,8 @@ dependencies: path: plugins/aves_magnifier aves_map: path: plugins/aves_map + aves_model: + path: plugins/aves_model aves_report: path: plugins/aves_report aves_report_platform: @@ -37,6 +39,8 @@ dependencies: path: plugins/aves_services aves_services_platform: path: plugins/aves_services_google + aves_video: + path: plugins/aves_video aves_ui: path: plugins/aves_ui charts_flutter: diff --git a/test/fake/media_fetch_service.dart b/test/fake/media_fetch_service.dart index fa9645ace..50dd1d969 100644 --- a/test/fake/media_fetch_service.dart +++ b/test/fake/media_fetch_service.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/media/media_fetch_service.dart'; import 'package:collection/collection.dart'; import 'package:test/fake.dart'; diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index eb9082ee2..d2f2ba4ef 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -1,4 +1,5 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/entry/origins.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/media/media_store_service.dart'; diff --git a/test/fake/metadata_db.dart b/test/fake/metadata_db.dart index 408ac2c3c..9f38ee70e 100644 --- a/test/fake/metadata_db.dart +++ b/test/fake/metadata_db.dart @@ -1,6 +1,6 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/db/db_metadata.dart'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; diff --git a/test/fake/metadata_fetch_service.dart b/test/fake/metadata_fetch_service.dart index 1891146ea..125ac4d6e 100644 --- a/test/fake/metadata_fetch_service.dart +++ b/test/fake/metadata_fetch_service.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/services/metadata/metadata_fetch_service.dart'; import 'package:flutter/foundation.dart'; diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index 20edbce4c..ecc7127af 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -4,6 +4,7 @@ import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/availability.dart'; import 'package:aves/model/covers.dart'; import 'package:aves/model/db/db_metadata.dart'; +import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/tag.dart'; diff --git a/test/utils/xmp_utils_test.dart b/test/utils/xmp_utils_test.dart index 489ee1677..84197b9c1 100644 --- a/test/utils/xmp_utils_test.dart +++ b/test/utils/xmp_utils_test.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/utils/xmp_utils.dart'; import 'package:test/test.dart'; import 'package:xml/xml.dart';