Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2023-03-17 19:58:11 +01:00
commit 77d374ea17
266 changed files with 2340 additions and 1301 deletions

View file

@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.8.4"></a>[v1.8.4] - 2023-03-17
### Added
- TV: improved support for Licenses
### Fixed
- Viewer: playing video from app content provider
- Search: using the query bar yields a black screen
## <a id="v1.8.3"></a>[v1.8.3] - 2023-03-13 ## <a id="v1.8.3"></a>[v1.8.3] - 2023-03-13
### Added ### Added

View file

@ -0,0 +1,5 @@
In v1.8.4:
- view items in full-screen when selecting them
- watch videos using picture-in-picture
- navigate with TalkBack
Full changelog available on GitHub

View file

@ -0,0 +1,5 @@
In v1.8.4:
- view items in full-screen when selecting them
- watch videos using picture-in-picture
- navigate with TalkBack
Full changelog available on GitHub

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/address.dart';

View file

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/db/db_metadata.dart'; import 'package:aves/model/db/db_metadata.dart';
import 'package:aves/model/db/db_metadata_sqflite_upgrade.dart'; import 'package:aves/model/db/db_metadata_sqflite_upgrade.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/address.dart';

View file

@ -2,51 +2,42 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/geo/countries.dart'; import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/entry/dirs.dart';
import 'package:aves/model/entry_dirs.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/geotiff.dart';
import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/multipage.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/trash.dart'; import 'package:aves/model/source/trash.dart';
import 'package:aves/model/video/metadata.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/service_policy.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/geocoding_service.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart';
import 'package:aves/theme/format.dart'; import 'package:aves/theme/format.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves_utils/aves_utils.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:country_code/country_code.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
enum EntryDataType { basic, aspectRatio, catalog, address, references } enum EntryDataType { basic, aspectRatio, catalog, address, references }
class EntryOrigins { class AvesEntry with AvesEntryBase {
static const int mediaStoreContent = 0; @override
static const int unknownContent = 1;
static const int file = 2;
static const int vault = 3;
}
class AvesEntry {
// `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode
int id; int id;
@override
String uri; String uri;
@override
int? pageId;
@override
int? sizeBytes;
String? _path, _filename, _extension, _sourceTitle; String? _path, _filename, _extension, _sourceTitle;
EntryDir? _directory; EntryDir? _directory;
int? pageId, contentId; int? contentId;
final String sourceMimeType; final String sourceMimeType;
int width, height, sourceRotationDegrees; int width, height, sourceRotationDegrees;
int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; int? dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
bool trashed; bool trashed;
int origin; int origin;
@ -57,7 +48,11 @@ class AvesEntry {
List<AvesEntry>? burstEntries; List<AvesEntry>? burstEntries;
final AChangeNotifier visualChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier(); @override
final AChangeNotifier visualChangeNotifier = AChangeNotifier();
final AChangeNotifier metadataChangeNotifier = AChangeNotifier();
final AChangeNotifier addressChangeNotifier = AChangeNotifier();
AvesEntry({ AvesEntry({
required int? id, required int? id,
@ -243,140 +238,8 @@ class AvesEntry {
// so we use the one found during cataloguing if possible // so we use the one found during cataloguing if possible
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType; String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
bool get isFavourite => favourites.isFavourite(this);
bool get isSvg => mimeType == MimeTypes.svg;
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
bool get _supportedByBitmapRegionDecoder =>
[
MimeTypes.heic,
MimeTypes.heif,
MimeTypes.jpeg,
MimeTypes.png,
MimeTypes.webp,
MimeTypes.arw,
MimeTypes.cr2,
MimeTypes.nef,
MimeTypes.nrw,
MimeTypes.orf,
MimeTypes.pef,
MimeTypes.raf,
MimeTypes.rw2,
MimeTypes.srw,
].contains(mimeType) &&
!isAnimated;
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
bool get useTiles => supportTiling && (width > 4096 || height > 4096);
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
bool get isImage => MimeTypes.isImage(mimeType);
bool get isVideo => MimeTypes.isVideo(mimeType);
bool get isCatalogued => _catalogMetadata != null; bool get isCatalogued => _catalogMetadata != null;
bool get isAnimated => _catalogMetadata?.isAnimated ?? false;
bool get isGeotiff => _catalogMetadata?.isGeotiff ?? false;
bool get is360 => _catalogMetadata?.is360 ?? false;
bool get isMediaStoreContent => uri.startsWith('content://media/');
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent);
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
bool get canEditLocation => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
bool get canEditTitleDescription => canEdit && canEditXmp;
bool get canEditRating => canEdit && canEditXmp;
bool get canEditTags => canEdit && canEditXmp;
bool get canRotate => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
bool get canFlip => canEdit && canEditExif;
bool get canEditExif => MimeTypes.canEditExif(mimeType);
bool get canEditIptc => MimeTypes.canEditIptc(mimeType);
bool get canEditXmp => MimeTypes.canEditXmp(mimeType);
bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType);
// Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata,
// so it should be registered as width=1920, height=1080, orientation=90,
// but is incorrectly registered as width=1080, height=1920, orientation=0.
// Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time.
// Comparing width and height can help with the portrait FHD video example,
// but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90
bool get isRotated => rotationDegrees % 180 == 90;
static const ratioSeparator = '\u2236';
static const resolutionSeparator = ' \u00D7 ';
bool get isSized => width > 0 && height > 0;
String get resolutionText {
final ws = width;
final hs = height;
return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs';
}
String get aspectRatioText {
if (width > 0 && height > 0) {
final gcd = width.gcd(height);
final w = width ~/ gcd;
final h = height ~/ gcd;
return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h';
} else {
return '?$ratioSeparator?';
}
}
double get displayAspectRatio {
if (width == 0 || height == 0) return 1;
return isRotated ? height / width : width / height;
}
Size get displaySize {
final w = width.toDouble();
final h = height.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
Size videoDisplaySize(double sar) {
final size = displaySize;
if (sar != 1) {
final dar = displayAspectRatio * sar;
final w = size.width;
final h = size.height;
if (w >= h) return Size(w, w / dar);
if (h > w) return Size(h * dar, h);
}
return size;
}
int get megaPixels => (width * height / 1000000).round();
DateTime? _bestDate; DateTime? _bestDate;
DateTime? get bestDate { DateTime? get bestDate {
@ -386,6 +249,7 @@ class AvesEntry {
int get rating => _catalogMetadata?.rating ?? 0; int get rating => _catalogMetadata?.rating ?? 0;
@override
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees; int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
set rotationDegrees(int rotationDegrees) { set rotationDegrees(int rotationDegrees) {
@ -397,6 +261,27 @@ class AvesEntry {
set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped; set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped;
// Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata,
// so it should be registered as width=1920, height=1080, orientation=90,
// but is incorrectly registered as width=1080, height=1920, orientation=0.
// Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time.
// Comparing width and height can help with the portrait FHD video example,
// but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90
bool get isRotated => rotationDegrees % 180 == 90;
@override
double get displayAspectRatio {
if (width == 0 || height == 0) return 1;
return isRotated ? height / width : width / height;
}
@override
Size get displaySize {
final w = width.toDouble();
final h = height.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
String? get sourceTitle => _sourceTitle; String? get sourceTitle => _sourceTitle;
set sourceTitle(String? sourceTitle) { set sourceTitle(String? sourceTitle) {
@ -423,6 +308,7 @@ class AvesEntry {
return d == null ? null : DateTime(d.year, d.month, d.day); return d == null ? null : DateTime(d.year, d.month, d.day);
} }
@override
int? get durationMillis => _durationMillis; int? get durationMillis => _durationMillis;
set durationMillis(int? durationMillis) { set durationMillis(int? durationMillis) {
@ -459,8 +345,6 @@ class AvesEntry {
// derived from Google reverse geocoding addresses // derived from Google reverse geocoding addresses
bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3; bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;
LatLng? get latLng => hasGps ? LatLng(_catalogMetadata!.latitude!, _catalogMetadata!.longitude!) : null;
Set<String>? _tags; Set<String>? _tags;
Set<String> get tags { Set<String> get tags {
@ -504,53 +388,6 @@ class AvesEntry {
addressDetails = null; addressDetails = null;
} }
Future<void> catalog({required bool background, required bool force, required bool persist}) async {
if (isCatalogued && !force) return;
if (isSvg) {
// vector image sizing is not essential, so we should not spend time for it during loading
// but it is useful anyway (for aspect ratios etc.) so we size them during cataloguing
final size = await SvgMetadataService.getSize(this);
if (size != null) {
final fields = {
'width': size.width.ceil(),
'height': size.height.ceil(),
};
await applyNewFields(fields, persist: persist);
}
catalogMetadata = CatalogMetadata(id: id);
} else {
// pre-processing
if (isVideo && (!isSized || durationMillis == 0)) {
// exotic video that is not sized during loading
final fields = await VideoMetadataFormatter.getLoadingMetadata(this);
await applyNewFields(fields, persist: persist);
}
// cataloguing on platform
catalogMetadata = await metadataFetchService.getCatalogMetadata(this, background: background);
// post-processing
if (isVideo && (catalogMetadata?.dateMillis ?? 0) == 0) {
catalogMetadata = await VideoMetadataFormatter.getCatalogMetadata(this);
}
if (isGeotiff && !hasGps) {
final info = await metadataFetchService.getGeoTiffInfo(this);
if (info != null) {
final center = MappedGeoTiff(
info: info,
entry: this,
).center;
if (center != null) {
catalogMetadata = catalogMetadata?.copyWith(
latitude: center.latitude,
longitude: center.longitude,
);
}
}
}
}
}
AddressDetails? get addressDetails => _addressDetails; AddressDetails? get addressDetails => _addressDetails;
set addressDetails(AddressDetails? newAddress) { set addressDetails(AddressDetails? newAddress) {
@ -558,79 +395,6 @@ class AvesEntry {
addressChangeNotifier.notify(); addressChangeNotifier.notify();
} }
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
if (hasGps) {
await _locateCountry(force: force);
if (await availability.canLocatePlaces) {
await locatePlace(background: background, force: force, geocoderLocale: geocoderLocale);
}
} else {
addressDetails = null;
}
}
// quick reverse geocoding to find the country, using an offline asset
Future<void> _locateCountry({required bool force}) async {
if (!hasGps || (hasAddress && !force)) return;
final countryCode = await countryTopology.countryCode(latLng!);
setCountry(countryCode);
}
void setCountry(CountryCode? countryCode) {
if (hasFineAddress || countryCode == null) return;
addressDetails = AddressDetails(
id: id,
countryCode: countryCode.alpha2,
countryName: countryCode.alpha3,
);
}
// full reverse geocoding, requiring Play Services and some connectivity
Future<void> locatePlace({required bool background, required bool force, required Locale geocoderLocale}) async {
if (!hasGps || (hasFineAddress && !force)) return;
try {
Future<List<Address>> call() => GeocodingService.getAddress(latLng!, geocoderLocale);
final addresses = await (background
? servicePolicy.call(
call,
priority: ServiceCallPriority.getLocation,
)
: call());
if (addresses.isNotEmpty) {
final address = addresses.first;
final cc = address.countryCode?.toUpperCase();
final cn = address.countryName;
final aa = address.adminArea;
addressDetails = AddressDetails(
id: id,
countryCode: cc,
countryName: cn,
adminArea: aa,
// if country & admin fields are null, it is likely the ocean,
// which is identified by `featureName` but we default to the address line anyway
locality: address.locality ?? (cc == null && cn == null && aa == null ? address.addressLine : null),
);
}
} catch (error, stack) {
debugPrint('$runtimeType locate failed with path=$path coordinates=$latLng error=$error\n$stack');
}
}
Future<String?> findAddressLine({required Locale geocoderLocale}) async {
if (!hasGps) return null;
try {
final addresses = await GeocodingService.getAddress(latLng!, geocoderLocale);
if (addresses.isNotEmpty) {
final address = addresses.first;
return address.addressLine;
}
} catch (error, stack) {
debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$latLng error=$error\n$stack');
}
return null;
}
String get shortAddress { String get shortAddress {
// `admin area` examples: Seoul, Geneva, null // `admin area` examples: Seoul, Geneva, null
// `locality` examples: Mapo-gu, Geneva, Annecy // `locality` examples: Mapo-gu, Geneva, Annecy
@ -732,107 +496,4 @@ class AvesEntry {
visualChangeNotifier.notify(); visualChangeNotifier.notify();
} }
} }
// favourites
Future<void> toggleFavourite() async {
if (isFavourite) {
await removeFromFavourites();
} else {
await addToFavourites();
}
}
Future<void> addToFavourites() async {
if (!isFavourite) {
await favourites.add({this});
}
}
Future<void> removeFromFavourites() async {
if (isFavourite) {
await favourites.removeEntries({this});
}
}
// multipage
static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
bool get isMultiPage => (_catalogMetadata?.isMultiPage ?? false) || isBurst;
bool get isBurst => burstEntries?.isNotEmpty == true;
// for backward compatibility
bool get _isMotionPhotoLegacy => isMultiPage && !isBurst && mimeType == MimeTypes.jpeg;
bool get isMotionPhoto => (_catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
String? get burstKey {
if (filenameWithoutExtension != null) {
final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
if (match != null) {
return '$directory/${match.group(1)}';
}
}
return null;
}
Future<MultiPageInfo?> getMultiPageInfo() async {
if (isBurst) {
return MultiPageInfo(
mainEntry: this,
pages: burstEntries!
.mapIndexed((index, entry) => SinglePageInfo(
index: index,
pageId: entry.id,
isDefault: index == 0,
uri: entry.uri,
mimeType: entry.mimeType,
width: entry.width,
height: entry.height,
rotationDegrees: entry.rotationDegrees,
durationMillis: entry.durationMillis,
))
.toList(),
);
} else {
return await metadataFetchService.getMultiPageInfo(this);
}
}
// sort
// compare by:
// 1) title ascending
// 2) extension ascending
static int compareByName(AvesEntry a, AvesEntry b) {
final c = compareAsciiUpperCaseNatural(a.bestTitle ?? '', b.bestTitle ?? '');
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
}
// compare by:
// 1) date descending
// 2) name descending
static int compareByDate(AvesEntry a, AvesEntry b) {
var c = (b.bestDate ?? epoch).compareTo(a.bestDate ?? epoch);
if (c != 0) return c;
return compareByName(b, a);
}
// compare by:
// 1) rating descending
// 2) date descending
static int compareByRating(AvesEntry a, AvesEntry b) {
final c = b.rating.compareTo(a.rating);
return c != 0 ? c : compareByDate(a, b);
}
// compare by:
// 1) size descending
// 2) date descending
static int compareBySize(AvesEntry a, AvesEntry b) {
final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0);
return c != 0 ? c : compareByDate(a, b);
}
} }

View file

@ -0,0 +1,56 @@
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';
extension ExtraAvesEntryCatalog on AvesEntry {
Future<void> catalog({required bool background, required bool force, required bool persist}) async {
if (isCatalogued && !force) return;
if (isSvg) {
// vector image sizing is not essential, so we should not spend time for it during loading
// but it is useful anyway (for aspect ratios etc.) so we size them during cataloguing
final size = await SvgMetadataService.getSize(this);
if (size != null) {
final fields = {
'width': size.width.ceil(),
'height': size.height.ceil(),
};
await applyNewFields(fields, persist: persist);
}
catalogMetadata = CatalogMetadata(id: id);
} else {
// pre-processing
if (isVideo && (!isSized || durationMillis == 0)) {
// exotic video that is not sized during loading
final fields = await VideoMetadataFormatter.getLoadingMetadata(this);
await applyNewFields(fields, persist: persist);
}
// cataloguing on platform
catalogMetadata = await metadataFetchService.getCatalogMetadata(this, background: background);
// post-processing
if (isVideo && (catalogMetadata?.dateMillis ?? 0) == 0) {
catalogMetadata = await VideoMetadataFormatter.getCatalogMetadata(this);
}
if (isGeotiff && !hasGps) {
final info = await metadataFetchService.getGeoTiffInfo(this);
if (info != null) {
final center = MappedGeoTiff(
info: info,
entry: this,
).center;
if (center != null) {
catalogMetadata = catalogMetadata?.copyWith(
latitude: center.latitude,
longitude: center.longitude,
);
}
}
}
}
}
}

View file

@ -0,0 +1,26 @@
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/favourites.dart';
extension ExtraAvesEntryFav on AvesEntry {
bool get isFavourite => favourites.isFavourite(this);
Future<void> toggleFavourite() async {
if (isFavourite) {
await removeFromFavourites();
} else {
await addToFavourites();
}
}
Future<void> addToFavourites() async {
if (!isFavourite) {
await favourites.add({this});
}
}
Future<void> removeFromFavourites() async {
if (isFavourite) {
await favourites.removeEntries({this});
}
}
}

View file

@ -3,8 +3,8 @@ import 'dart:math';
import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/image_providers/region_provider.dart';
import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/thumbnail_provider.dart';
import 'package:aves/image_providers/uri_image_provider.dart'; import 'package:aves/image_providers/uri_image_provider.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';

View file

@ -1,8 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/video/keys.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/video/metadata.dart'; import 'package:aves/model/video/metadata.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
@ -10,6 +11,7 @@ import 'package:aves/services/metadata/svg_metadata_service.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -79,27 +81,27 @@ extension ExtraAvesEntryInfo on AvesEntry {
if (mediaInfo.containsKey(Keys.streams)) { if (mediaInfo.containsKey(Keys.streams)) {
String getTypeText(Map stream) { String getTypeText(Map stream) {
final type = stream[Keys.streamType] ?? StreamTypes.unknown; final type = stream[Keys.streamType] ?? MediaStreamTypes.unknown;
switch (type) { switch (type) {
case StreamTypes.attachment: case MediaStreamTypes.attachment:
return 'Attachment'; return 'Attachment';
case StreamTypes.audio: case MediaStreamTypes.audio:
return 'Audio'; return 'Audio';
case StreamTypes.metadata: case MediaStreamTypes.metadata:
return 'Metadata'; return 'Metadata';
case StreamTypes.subtitle: case MediaStreamTypes.subtitle:
case StreamTypes.timedText: case MediaStreamTypes.timedText:
return 'Text'; return 'Text';
case StreamTypes.video: case MediaStreamTypes.video:
return stream.containsKey(Keys.fpsDen) ? 'Video' : 'Image'; return stream.containsKey(Keys.fpsDen) ? 'Video' : 'Image';
case StreamTypes.unknown: case MediaStreamTypes.unknown:
default: default:
return 'Unknown'; return 'Unknown';
} }
} }
final allStreams = (mediaInfo[Keys.streams] as List).cast<Map>(); final allStreams = (mediaInfo[Keys.streams] as List).cast<Map>();
final attachmentStreams = allStreams.where((stream) => stream[Keys.streamType] == StreamTypes.attachment).toList(); final attachmentStreams = allStreams.where((stream) => stream[Keys.streamType] == MediaStreamTypes.attachment).toList();
final knownStreams = allStreams.whereNot(attachmentStreams.contains); final knownStreams = allStreams.whereNot(attachmentStreams.contains);
// display known streams as separate directories (e.g. video, audio, subs) // display known streams as separate directories (e.g. video, audio, subs)

View file

@ -0,0 +1,89 @@
import 'dart:async';
import 'dart:ui';
import 'package:aves/geo/countries.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/services/common/service_policy.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/geocoding_service.dart';
import 'package:country_code/country_code.dart';
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
extension ExtraAvesEntryLocation on AvesEntry {
LatLng? get latLng => hasGps ? LatLng(catalogMetadata!.latitude!, catalogMetadata!.longitude!) : null;
Future<void> locate({required bool background, required bool force, required Locale geocoderLocale}) async {
if (hasGps) {
await _locateCountry(force: force);
if (await availability.canLocatePlaces) {
await locatePlace(background: background, force: force, geocoderLocale: geocoderLocale);
}
} else {
addressDetails = null;
}
}
// quick reverse geocoding to find the country, using an offline asset
Future<void> _locateCountry({required bool force}) async {
if (!hasGps || (hasAddress && !force)) return;
final countryCode = await countryTopology.countryCode(latLng!);
setCountry(countryCode);
}
void setCountry(CountryCode? countryCode) {
if (hasFineAddress || countryCode == null) return;
addressDetails = AddressDetails(
id: id,
countryCode: countryCode.alpha2,
countryName: countryCode.alpha3,
);
}
// full reverse geocoding, requiring Play Services and some connectivity
Future<void> locatePlace({required bool background, required bool force, required Locale geocoderLocale}) async {
if (!hasGps || (hasFineAddress && !force)) return;
try {
Future<List<Address>> call() => GeocodingService.getAddress(latLng!, geocoderLocale);
final addresses = await (background
? servicePolicy.call(
call,
priority: ServiceCallPriority.getLocation,
)
: call());
if (addresses.isNotEmpty) {
final address = addresses.first;
final cc = address.countryCode?.toUpperCase();
final cn = address.countryName;
final aa = address.adminArea;
addressDetails = AddressDetails(
id: id,
countryCode: cc,
countryName: cn,
adminArea: aa,
// if country & admin fields are null, it is likely the ocean,
// which is identified by `featureName` but we default to the address line anyway
locality: address.locality ?? (cc == null && cn == null && aa == null ? address.addressLine : null),
);
}
} catch (error, stack) {
debugPrint('$runtimeType locate failed with path=$path coordinates=$latLng error=$error\n$stack');
}
}
Future<String?> findAddressLine({required Locale geocoderLocale}) async {
if (!hasGps) return null;
try {
final addresses = await GeocodingService.getAddress(latLng!, geocoderLocale);
if (addresses.isNotEmpty) {
final address = addresses.first;
return address.addressLine;
}
} catch (error, stack) {
debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$latLng error=$error\n$stack');
}
return null;
}
}

View file

@ -1,7 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
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/props.dart';
import 'package:aves/model/metadata/date_modifier.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/date_field_source.dart';
import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/model/metadata/enums/enums.dart';

View file

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/multipage.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
extension ExtraAvesEntryMultipage on AvesEntry {
static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
bool get isMultiPage => (catalogMetadata?.isMultiPage ?? false) || isBurst;
bool get isBurst => burstEntries?.isNotEmpty == true;
// for backward compatibility
bool get _isMotionPhotoLegacy => isMultiPage && !isBurst && mimeType == MimeTypes.jpeg;
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
String? get burstKey {
if (filenameWithoutExtension != null) {
final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
if (match != null) {
return '$directory/${match.group(1)}';
}
}
return null;
}
Future<MultiPageInfo?> getMultiPageInfo() async {
if (isBurst) {
return MultiPageInfo(
mainEntry: this,
pages: burstEntries!
.mapIndexed((index, entry) => SinglePageInfo(
index: index,
pageId: entry.id,
isDefault: index == 0,
uri: entry.uri,
mimeType: entry.mimeType,
width: entry.width,
height: entry.height,
rotationDegrees: entry.rotationDegrees,
durationMillis: entry.durationMillis,
))
.toList(),
);
} else {
return await metadataFetchService.getMultiPageInfo(this);
}
}
}

View file

@ -0,0 +1,119 @@
import 'dart:ui';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/utils/android_file_utils.dart';
extension ExtraAvesEntryProps on AvesEntry {
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
bool get isSvg => mimeType == MimeTypes.svg;
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
bool get _supportedByBitmapRegionDecoder =>
[
MimeTypes.heic,
MimeTypes.heif,
MimeTypes.jpeg,
MimeTypes.png,
MimeTypes.webp,
MimeTypes.arw,
MimeTypes.cr2,
MimeTypes.nef,
MimeTypes.nrw,
MimeTypes.orf,
MimeTypes.pef,
MimeTypes.raf,
MimeTypes.rw2,
MimeTypes.srw,
].contains(mimeType) &&
!isAnimated;
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
bool get useTiles => supportTiling && (width > 4096 || height > 4096);
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
bool get isImage => MimeTypes.isImage(mimeType);
bool get isVideo => MimeTypes.isVideo(mimeType);
bool get isAnimated => catalogMetadata?.isAnimated ?? false;
bool get isGeotiff => catalogMetadata?.isGeotiff ?? false;
bool get is360 => catalogMetadata?.is360 ?? false;
bool get isMediaStoreContent => uri.startsWith('content://media/');
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent);
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
bool get canEditLocation => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
bool get canEditTitleDescription => canEdit && canEditXmp;
bool get canEditRating => canEdit && canEditXmp;
bool get canEditTags => canEdit && canEditXmp;
bool get canRotate => canEdit && (canEditExif || mimeType == MimeTypes.mp4);
bool get canFlip => canEdit && canEditExif;
bool get canEditExif => MimeTypes.canEditExif(mimeType);
bool get canEditIptc => MimeTypes.canEditIptc(mimeType);
bool get canEditXmp => MimeTypes.canEditXmp(mimeType);
bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType);
static const ratioSeparator = '\u2236';
static const resolutionSeparator = ' \u00D7 ';
bool get isSized => width > 0 && height > 0;
String get resolutionText {
final ws = width;
final hs = height;
return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs';
}
String get aspectRatioText {
if (width > 0 && height > 0) {
final gcd = width.gcd(height);
final w = width ~/ gcd;
final h = height ~/ gcd;
return isRotated ? '$h$ratioSeparator$w' : '$w$ratioSeparator$h';
} else {
return '?$ratioSeparator?';
}
}
Size videoDisplaySize(double sar) {
final size = displaySize;
if (sar != 1) {
final dar = displayAspectRatio * sar;
final w = size.width;
final h = size.height;
if (w >= h) return Size(w, w / dar);
if (h > w) return Size(h * dar, h);
}
return size;
}
int get megaPixels => (width * height / 1000000).round();
}

View file

@ -0,0 +1,6 @@
class EntryOrigins {
static const int mediaStoreContent = 0;
static const int unknownContent = 1;
static const int file = 2;
static const int vault = 3;
}

38
lib/model/entry/sort.dart Normal file
View file

@ -0,0 +1,38 @@
import 'package:aves/model/entry/entry.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:collection/collection.dart';
class AvesEntrySort {
// compare by:
// 1) title ascending
// 2) extension ascending
static int compareByName(AvesEntry a, AvesEntry b) {
final c = compareAsciiUpperCaseNatural(a.bestTitle ?? '', b.bestTitle ?? '');
return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? '');
}
// compare by:
// 1) date descending
// 2) name descending
static int compareByDate(AvesEntry a, AvesEntry b) {
var c = (b.bestDate ?? epoch).compareTo(a.bestDate ?? epoch);
if (c != 0) return c;
return compareByName(b, a);
}
// compare by:
// 1) rating descending
// 2) date descending
static int compareByRating(AvesEntry a, AvesEntry b) {
final c = b.rating.compareTo(a.rating);
return c != 0 ? c : compareByDate(a, b);
}
// compare by:
// 1) size descending
// 2) date descending
static int compareBySize(AvesEntry a, AvesEntry b) {
final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0);
return c != 0 ? c : compareByDate(a, b);
}
}

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';

View file

@ -1,4 +1,5 @@
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/aspect_ratio.dart'; import 'package:aves/model/filters/aspect_ratio.dart';
import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/coordinate.dart';

View file

@ -1,4 +1,6 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';

View file

@ -1,3 +1,5 @@
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -2,8 +2,8 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry_images.dart'; import 'package:aves/model/entry/extensions/images.dart';
import 'package:aves/ref/geotiff.dart'; import 'package:aves/ref/geotiff.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/utils/change_notifier.dart'; import 'package:aves_utils/aves_utils.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class Query extends ChangeNotifier { class Query extends ChangeNotifier {

View file

@ -1,21 +1,7 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraAccessibilityAnimations on AccessibilityAnimations { extension ExtraAccessibilityAnimations on AccessibilityAnimations {
String getName(BuildContext context) {
switch (this) {
case AccessibilityAnimations.system:
return context.l10n.settingsSystemDefault;
case AccessibilityAnimations.disabled:
return context.l10n.accessibilityAnimationsRemove;
case AccessibilityAnimations.enabled:
return context.l10n.accessibilityAnimationsKeep;
}
}
bool get animate { bool get animate {
switch (this) { switch (this) {
case AccessibilityAnimations.system: case AccessibilityAnimations.system:

View file

@ -1,23 +0,0 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraAccessibilityTimeout on AccessibilityTimeout {
String getName(BuildContext context) {
switch (this) {
case AccessibilityTimeout.system:
return context.l10n.settingsSystemDefault;
case AccessibilityTimeout.s1:
return context.l10n.timeSeconds(1);
case AccessibilityTimeout.s3:
return context.l10n.timeSeconds(3);
case AccessibilityTimeout.s5:
return context.l10n.timeSeconds(5);
case AccessibilityTimeout.s10:
return context.l10n.timeSeconds(10);
case AccessibilityTimeout.s30:
return context.l10n.timeSeconds(30);
}
}
}

View file

@ -1,21 +1,9 @@
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'enums.dart';
extension ExtraCoordinateFormat on CoordinateFormat { extension ExtraCoordinateFormat on CoordinateFormat {
String getName(BuildContext context) {
switch (this) {
case CoordinateFormat.dms:
return context.l10n.coordinateFormatDms;
case CoordinateFormat.decimal:
return context.l10n.coordinateFormatDecimal;
}
}
static const _separator = ', '; static const _separator = ', ';
String format(AppLocalizations l10n, LatLng latLng, {bool minuteSecondPadding = false, int dmsSecondDecimals = 2}) { String format(AppLocalizations l10n, LatLng latLng, {bool minuteSecondPadding = false, int dmsSecondDecimals = 2}) {

View file

@ -1,23 +1,10 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'enums.dart';
extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode { extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode {
String getName(BuildContext context) {
switch (this) {
case DisplayRefreshRateMode.auto:
return context.l10n.settingsSystemDefault;
case DisplayRefreshRateMode.highest:
return context.l10n.displayRefreshRatePreferHighest;
case DisplayRefreshRateMode.lowest:
return context.l10n.displayRefreshRatePreferLowest;
}
}
Future<void> apply() async { Future<void> apply() async {
if (!await windowService.isActivity()) return; if (!await windowService.isActivity()) return;

View file

@ -1,7 +1,6 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'enums.dart';
extension ExtraEntryBackground on EntryBackground { extension ExtraEntryBackground on EntryBackground {
bool get isColor { bool get isColor {
switch (this) { switch (this) {

View file

@ -1,20 +1,8 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraHomePageSetting on HomePageSetting { extension ExtraHomePageSetting on HomePageSetting {
String getName(BuildContext context) {
switch (this) {
case HomePageSetting.collection:
return context.l10n.drawerCollectionAll;
case HomePageSetting.albums:
return context.l10n.drawerAlbumPage;
}
}
String get routeName { String get routeName {
switch (this) { switch (this) {
case HomePageSetting.collection: case HomePageSetting.collection:

View file

@ -0,0 +1,278 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/widgets.dart';
extension ExtraAccessibilityAnimationsName on AccessibilityAnimations {
String getName(BuildContext context) {
switch (this) {
case AccessibilityAnimations.system:
return context.l10n.settingsSystemDefault;
case AccessibilityAnimations.disabled:
return context.l10n.accessibilityAnimationsRemove;
case AccessibilityAnimations.enabled:
return context.l10n.accessibilityAnimationsKeep;
}
}
}
extension ExtraAccessibilityTimeoutName on AccessibilityTimeout {
String getName(BuildContext context) {
switch (this) {
case AccessibilityTimeout.system:
return context.l10n.settingsSystemDefault;
case AccessibilityTimeout.s1:
return context.l10n.timeSeconds(1);
case AccessibilityTimeout.s3:
return context.l10n.timeSeconds(3);
case AccessibilityTimeout.s5:
return context.l10n.timeSeconds(5);
case AccessibilityTimeout.s10:
return context.l10n.timeSeconds(10);
case AccessibilityTimeout.s30:
return context.l10n.timeSeconds(30);
}
}
}
extension ExtraAvesThemeBrightnessName on AvesThemeBrightness {
String getName(BuildContext context) {
switch (this) {
case AvesThemeBrightness.system:
return context.l10n.settingsSystemDefault;
case AvesThemeBrightness.light:
return context.l10n.themeBrightnessLight;
case AvesThemeBrightness.dark:
return context.l10n.themeBrightnessDark;
case AvesThemeBrightness.black:
return context.l10n.themeBrightnessBlack;
}
}
}
extension ExtraCoordinateFormatName on CoordinateFormat {
String getName(BuildContext context) {
switch (this) {
case CoordinateFormat.dms:
return context.l10n.coordinateFormatDms;
case CoordinateFormat.decimal:
return context.l10n.coordinateFormatDecimal;
}
}
}
extension ExtraDisplayRefreshRateModeName on DisplayRefreshRateMode {
String getName(BuildContext context) {
switch (this) {
case DisplayRefreshRateMode.auto:
return context.l10n.settingsSystemDefault;
case DisplayRefreshRateMode.highest:
return context.l10n.displayRefreshRatePreferHighest;
case DisplayRefreshRateMode.lowest:
return context.l10n.displayRefreshRatePreferLowest;
}
}
}
extension ExtraEntryMapStyleName on EntryMapStyle {
String getName(BuildContext context) {
switch (this) {
case EntryMapStyle.googleNormal:
return context.l10n.mapStyleGoogleNormal;
case EntryMapStyle.googleHybrid:
return context.l10n.mapStyleGoogleHybrid;
case EntryMapStyle.googleTerrain:
return context.l10n.mapStyleGoogleTerrain;
case EntryMapStyle.hmsNormal:
return context.l10n.mapStyleHuaweiNormal;
case EntryMapStyle.hmsTerrain:
return context.l10n.mapStyleHuaweiTerrain;
case EntryMapStyle.osmHot:
return context.l10n.mapStyleOsmHot;
case EntryMapStyle.stamenToner:
return context.l10n.mapStyleStamenToner;
case EntryMapStyle.stamenWatercolor:
return context.l10n.mapStyleStamenWatercolor;
}
}
}
extension ExtraHomePageSettingName on HomePageSetting {
String getName(BuildContext context) {
switch (this) {
case HomePageSetting.collection:
return context.l10n.drawerCollectionAll;
case HomePageSetting.albums:
return context.l10n.drawerAlbumPage;
}
}
}
extension ExtraKeepScreenOnName on KeepScreenOn {
String getName(BuildContext context) {
switch (this) {
case KeepScreenOn.never:
return context.l10n.keepScreenOnNever;
case KeepScreenOn.videoPlayback:
return context.l10n.keepScreenOnVideoPlayback;
case KeepScreenOn.viewerOnly:
return context.l10n.keepScreenOnViewerOnly;
case KeepScreenOn.always:
return context.l10n.keepScreenOnAlways;
}
}
}
extension ExtraSlideshowVideoPlaybackName on SlideshowVideoPlayback {
String getName(BuildContext context) {
switch (this) {
case SlideshowVideoPlayback.skip:
return context.l10n.videoPlaybackSkip;
case SlideshowVideoPlayback.playMuted:
return context.l10n.videoPlaybackMuted;
case SlideshowVideoPlayback.playWithSound:
return context.l10n.videoPlaybackWithSound;
}
}
}
extension ExtraSubtitlePositionName on SubtitlePosition {
String getName(BuildContext context) {
switch (this) {
case SubtitlePosition.top:
return context.l10n.subtitlePositionTop;
case SubtitlePosition.bottom:
return context.l10n.subtitlePositionBottom;
}
}
}
extension ExtraThumbnailOverlayLocationIconName on ThumbnailOverlayLocationIcon {
String getName(BuildContext context) {
switch (this) {
case ThumbnailOverlayLocationIcon.located:
return context.l10n.filterLocatedLabel;
case ThumbnailOverlayLocationIcon.unlocated:
return context.l10n.filterNoLocationLabel;
case ThumbnailOverlayLocationIcon.none:
return context.l10n.settingsDisabled;
}
}
}
extension ExtraThumbnailOverlayTagIconName on ThumbnailOverlayTagIcon {
String getName(BuildContext context) {
switch (this) {
case ThumbnailOverlayTagIcon.tagged:
return context.l10n.filterTaggedLabel;
case ThumbnailOverlayTagIcon.untagged:
return context.l10n.filterNoTagLabel;
case ThumbnailOverlayTagIcon.none:
return context.l10n.settingsDisabled;
}
}
}
extension ExtraUnitSystemName on UnitSystem {
String getName(BuildContext context) {
switch (this) {
case UnitSystem.metric:
return context.l10n.unitSystemMetric;
case UnitSystem.imperial:
return context.l10n.unitSystemImperial;
}
}
}
extension ExtraVideoAutoPlayModeName on VideoAutoPlayMode {
String getName(BuildContext context) {
switch (this) {
case VideoAutoPlayMode.disabled:
return context.l10n.settingsDisabled;
case VideoAutoPlayMode.playMuted:
return context.l10n.videoPlaybackMuted;
case VideoAutoPlayMode.playWithSound:
return context.l10n.videoPlaybackWithSound;
}
}
}
extension ExtraVideoBackgroundModeName on VideoBackgroundMode {
String getName(BuildContext context) {
switch (this) {
case VideoBackgroundMode.disabled:
return context.l10n.settingsDisabled;
case VideoBackgroundMode.pip:
return context.l10n.settingsVideoEnablePip;
}
}
}
extension ExtraVideoControlsName on VideoControls {
String getName(BuildContext context) {
switch (this) {
case VideoControls.play:
return context.l10n.videoControlsPlay;
case VideoControls.playSeek:
return context.l10n.videoControlsPlaySeek;
case VideoControls.playOutside:
return context.l10n.videoControlsPlayOutside;
case VideoControls.none:
return context.l10n.videoControlsNone;
}
}
}
extension ExtraVideoLoopModeName on VideoLoopMode {
String getName(BuildContext context) {
switch (this) {
case VideoLoopMode.never:
return context.l10n.videoLoopModeNever;
case VideoLoopMode.shortOnly:
return context.l10n.videoLoopModeShortOnly;
case VideoLoopMode.always:
return context.l10n.videoLoopModeAlways;
}
}
}
extension ExtraViewerTransitionName on ViewerTransition {
String getName(BuildContext context) {
switch (this) {
case ViewerTransition.slide:
return context.l10n.viewerTransitionSlide;
case ViewerTransition.parallax:
return context.l10n.viewerTransitionParallax;
case ViewerTransition.fade:
return context.l10n.viewerTransitionFade;
case ViewerTransition.zoomIn:
return context.l10n.viewerTransitionZoomIn;
case ViewerTransition.none:
return context.l10n.viewerTransitionNone;
}
}
}
extension ExtraWidgetDisplayedItemName on WidgetDisplayedItem {
String getName(BuildContext context) {
switch (this) {
case WidgetDisplayedItem.random:
return context.l10n.widgetDisplayedItemRandom;
case WidgetDisplayedItem.mostRecent:
return context.l10n.widgetDisplayedItemMostRecent;
}
}
}
extension ExtraWidgetOpenPageName on WidgetOpenPage {
String getName(BuildContext context) {
switch (this) {
case WidgetOpenPage.home:
return context.l10n.widgetOpenPageHome;
case WidgetOpenPage.collection:
return context.l10n.widgetOpenPageCollection;
case WidgetOpenPage.viewer:
return context.l10n.widgetOpenPageViewer;
}
}
}

View file

@ -1,6 +1,4 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/widgets.dart';
extension ExtraEntryMapStyle on EntryMapStyle { extension ExtraEntryMapStyle on EntryMapStyle {
static bool isHeavy(EntryMapStyle? style) { static bool isHeavy(EntryMapStyle? style) {
@ -16,27 +14,6 @@ extension ExtraEntryMapStyle on EntryMapStyle {
} }
} }
String getName(BuildContext context) {
switch (this) {
case EntryMapStyle.googleNormal:
return context.l10n.mapStyleGoogleNormal;
case EntryMapStyle.googleHybrid:
return context.l10n.mapStyleGoogleHybrid;
case EntryMapStyle.googleTerrain:
return context.l10n.mapStyleGoogleTerrain;
case EntryMapStyle.hmsNormal:
return context.l10n.mapStyleHuaweiNormal;
case EntryMapStyle.hmsTerrain:
return context.l10n.mapStyleHuaweiTerrain;
case EntryMapStyle.osmHot:
return context.l10n.mapStyleOsmHot;
case EntryMapStyle.stamenToner:
return context.l10n.mapStyleStamenToner;
case EntryMapStyle.stamenWatercolor:
return context.l10n.mapStyleStamenWatercolor;
}
}
bool get needMobileService { bool get needMobileService {
switch (this) { switch (this) {
case EntryMapStyle.osmHot: case EntryMapStyle.osmHot:

View file

@ -1,23 +1,7 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraKeepScreenOn on KeepScreenOn { extension ExtraKeepScreenOn on KeepScreenOn {
String getName(BuildContext context) {
switch (this) {
case KeepScreenOn.never:
return context.l10n.keepScreenOnNever;
case KeepScreenOn.videoPlayback:
return context.l10n.keepScreenOnVideoPlayback;
case KeepScreenOn.viewerOnly:
return context.l10n.keepScreenOnViewerOnly;
case KeepScreenOn.always:
return context.l10n.keepScreenOnAlways;
}
}
void apply() { void apply() {
windowService.keepScreenOn(this == KeepScreenOn.always); windowService.keepScreenOn(this == KeepScreenOn.always);
} }

View file

@ -1,17 +0,0 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraSlideshowVideoPlayback on SlideshowVideoPlayback {
String getName(BuildContext context) {
switch (this) {
case SlideshowVideoPlayback.skip:
return context.l10n.videoPlaybackSkip;
case SlideshowVideoPlayback.playMuted:
return context.l10n.videoPlaybackMuted;
case SlideshowVideoPlayback.playWithSound:
return context.l10n.videoPlaybackWithSound;
}
}
}

View file

@ -1,18 +1,7 @@
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraSubtitlePosition on SubtitlePosition { extension ExtraSubtitlePosition on SubtitlePosition {
String getName(BuildContext context) {
switch (this) {
case SubtitlePosition.top:
return context.l10n.subtitlePositionTop;
case SubtitlePosition.bottom:
return context.l10n.subtitlePositionBottom;
}
}
TextAlignVertical toTextAlignVertical() { TextAlignVertical toTextAlignVertical() {
switch (this) { switch (this) {
case SubtitlePosition.top: case SubtitlePosition.top:

View file

@ -1,22 +1,7 @@
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'enums.dart';
extension ExtraAvesThemeBrightness on AvesThemeBrightness { extension ExtraAvesThemeBrightness on AvesThemeBrightness {
String getName(BuildContext context) {
switch (this) {
case AvesThemeBrightness.system:
return context.l10n.settingsSystemDefault;
case AvesThemeBrightness.light:
return context.l10n.themeBrightnessLight;
case AvesThemeBrightness.dark:
return context.l10n.themeBrightnessDark;
case AvesThemeBrightness.black:
return context.l10n.themeBrightnessBlack;
}
}
ThemeMode get appThemeMode { ThemeMode get appThemeMode {
switch (this) { switch (this) {
case AvesThemeBrightness.system: case AvesThemeBrightness.system:

View file

@ -1,21 +1,8 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'enums.dart';
extension ExtraThumbnailOverlayLocationIcon on ThumbnailOverlayLocationIcon { extension ExtraThumbnailOverlayLocationIcon on ThumbnailOverlayLocationIcon {
String getName(BuildContext context) {
switch (this) {
case ThumbnailOverlayLocationIcon.located:
return context.l10n.filterLocatedLabel;
case ThumbnailOverlayLocationIcon.unlocated:
return context.l10n.filterNoLocationLabel;
case ThumbnailOverlayLocationIcon.none:
return context.l10n.settingsDisabled;
}
}
IconData getIcon(BuildContext context) { IconData getIcon(BuildContext context) {
switch (this) { switch (this) {
case ThumbnailOverlayLocationIcon.unlocated: case ThumbnailOverlayLocationIcon.unlocated:

View file

@ -1,21 +1,8 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'enums.dart';
extension ExtraThumbnailOverlayTagIcon on ThumbnailOverlayTagIcon { extension ExtraThumbnailOverlayTagIcon on ThumbnailOverlayTagIcon {
String getName(BuildContext context) {
switch (this) {
case ThumbnailOverlayTagIcon.tagged:
return context.l10n.filterTaggedLabel;
case ThumbnailOverlayTagIcon.untagged:
return context.l10n.filterNoTagLabel;
case ThumbnailOverlayTagIcon.none:
return context.l10n.settingsDisabled;
}
}
IconData getIcon(BuildContext context) { IconData getIcon(BuildContext context) {
switch (this) { switch (this) {
case ThumbnailOverlayTagIcon.tagged: case ThumbnailOverlayTagIcon.tagged:

View file

@ -1,15 +0,0 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraUnitSystem on UnitSystem {
String getName(BuildContext context) {
switch (this) {
case UnitSystem.metric:
return context.l10n.unitSystemMetric;
case UnitSystem.imperial:
return context.l10n.unitSystemImperial;
}
}
}

View file

@ -1,17 +0,0 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraVideoAutoPlayMode on VideoAutoPlayMode {
String getName(BuildContext context) {
switch (this) {
case VideoAutoPlayMode.disabled:
return context.l10n.settingsDisabled;
case VideoAutoPlayMode.playMuted:
return context.l10n.videoPlaybackMuted;
case VideoAutoPlayMode.playWithSound:
return context.l10n.videoPlaybackWithSound;
}
}
}

View file

@ -1,15 +0,0 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraVideoBackgroundMode on VideoBackgroundMode {
String getName(BuildContext context) {
switch (this) {
case VideoBackgroundMode.disabled:
return context.l10n.settingsDisabled;
case VideoBackgroundMode.pip:
return context.l10n.settingsVideoEnablePip;
}
}
}

View file

@ -1,19 +0,0 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraVideoControls on VideoControls {
String getName(BuildContext context) {
switch (this) {
case VideoControls.play:
return context.l10n.videoControlsPlay;
case VideoControls.playSeek:
return context.l10n.videoControlsPlaySeek;
case VideoControls.playOutside:
return context.l10n.videoControlsPlayOutside;
case VideoControls.none:
return context.l10n.videoControlsNone;
}
}
}

View file

@ -1,29 +1,13 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraVideoLoopMode on VideoLoopMode { extension ExtraVideoLoopMode on VideoLoopMode {
String getName(BuildContext context) {
switch (this) {
case VideoLoopMode.never:
return context.l10n.videoLoopModeNever;
case VideoLoopMode.shortOnly:
return context.l10n.videoLoopModeShortOnly;
case VideoLoopMode.always:
return context.l10n.videoLoopModeAlways;
}
}
static const shortVideoThreshold = Duration(seconds: 30); static const shortVideoThreshold = Duration(seconds: 30);
bool shouldLoop(AvesEntry entry) { bool shouldLoop(int? durationMillis) {
switch (this) { switch (this) {
case VideoLoopMode.never: case VideoLoopMode.never:
return false; return false;
case VideoLoopMode.shortOnly: case VideoLoopMode.shortOnly:
final durationMillis = entry.durationMillis;
return durationMillis != null ? durationMillis < shortVideoThreshold.inMilliseconds : false; return durationMillis != null ? durationMillis < shortVideoThreshold.inMilliseconds : false;
case VideoLoopMode.always: case VideoLoopMode.always:
return true; return true;

View file

@ -1,25 +1,8 @@
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraViewerTransition on ViewerTransition { extension ExtraViewerTransition on ViewerTransition {
String getName(BuildContext context) {
switch (this) {
case ViewerTransition.slide:
return context.l10n.viewerTransitionSlide;
case ViewerTransition.parallax:
return context.l10n.viewerTransitionParallax;
case ViewerTransition.fade:
return context.l10n.viewerTransitionFade;
case ViewerTransition.zoomIn:
return context.l10n.viewerTransitionZoomIn;
case ViewerTransition.none:
return context.l10n.viewerTransitionNone;
}
}
TransitionBuilder builder(PageController pageController, int index) { TransitionBuilder builder(PageController pageController, int index) {
switch (this) { switch (this) {
case ViewerTransition.slide: case ViewerTransition.slide:

View file

@ -1,14 +0,0 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
extension ExtraWidgetDisplayedItem on WidgetDisplayedItem {
String getName(BuildContext context) {
switch (this) {
case WidgetDisplayedItem.random:
return context.l10n.widgetDisplayedItemRandom;
case WidgetDisplayedItem.mostRecent:
return context.l10n.widgetDisplayedItemMostRecent;
}
}
}

View file

@ -1,16 +0,0 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
extension ExtraWidgetOpenPage on WidgetOpenPage {
String getName(BuildContext context) {
switch (this) {
case WidgetOpenPage.home:
return context.l10n.widgetOpenPageHome;
case WidgetOpenPage.collection:
return context.l10n.widgetOpenPageCollection;
case WidgetOpenPage.viewer:
return context.l10n.widgetOpenPageViewer;
}
}
}

View file

@ -1,6 +1,6 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart';
extension ExtraWidgetShape on WidgetShape { extension ExtraWidgetShape on WidgetShape {
Path path(Size widgetSize, double devicePixelRatio) { Path path(Size widgetSize, double devicePixelRatio) {

View file

@ -14,7 +14,7 @@ import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/accessibility_service.dart';
import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves_utils/aves_utils.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/search/page.dart'; import 'package:aves/widgets/common/search/page.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';

View file

@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/sort.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
@ -13,17 +15,16 @@ import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location/location.dart'; import 'package:aves/model/source/location/location.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'enums/enums.dart';
class CollectionLens with ChangeNotifier { class CollectionLens with ChangeNotifier {
final CollectionSource source; final CollectionSource source;
final Set<CollectionFilter> filters; final Set<CollectionFilter> filters;
@ -190,7 +191,7 @@ class CollectionLens with ChangeNotifier {
final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey(); final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey();
byBurstKey.forEach((burstKey, entries) { byBurstKey.forEach((burstKey, entries) {
if (entries.length > 1) { if (entries.length > 1) {
entries.sort(AvesEntry.compareByName); entries.sort(AvesEntrySort.compareByName);
final mainEntry = entries.first; final mainEntry = entries.first;
final burstEntry = mainEntry.copyWith(burstEntries: entries); final burstEntry = mainEntry.copyWith(burstEntries: entries);
@ -209,16 +210,16 @@ class CollectionLens with ChangeNotifier {
switch (sortFactor) { switch (sortFactor) {
case EntrySortFactor.date: case EntrySortFactor.date:
_filteredSortedEntries.sort(AvesEntry.compareByDate); _filteredSortedEntries.sort(AvesEntrySort.compareByDate);
break; break;
case EntrySortFactor.name: case EntrySortFactor.name:
_filteredSortedEntries.sort(AvesEntry.compareByName); _filteredSortedEntries.sort(AvesEntrySort.compareByName);
break; break;
case EntrySortFactor.rating: case EntrySortFactor.rating:
_filteredSortedEntries.sort(AvesEntry.compareByRating); _filteredSortedEntries.sort(AvesEntrySort.compareByRating);
break; break;
case EntrySortFactor.size: case EntrySortFactor.size:
_filteredSortedEntries.sort(AvesEntry.compareBySize); _filteredSortedEntries.sort(AvesEntrySort.compareBySize);
break; break;
} }
if (sortReverse) { if (sortReverse) {

View file

@ -2,7 +2,10 @@ import 'dart:async';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/sort.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
@ -105,7 +108,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
@override @override
List<AvesEntry> get sortedEntriesByDate { List<AvesEntry> get sortedEntriesByDate {
_sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntry.compareByDate)); _sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntrySort.compareByDate));
return _sortedEntriesByDate!; return _sortedEntriesByDate!;
} }

View file

@ -1,9 +1,8 @@
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraEntrySortFactor on EntrySortFactor { extension ExtraEntrySortFactor on EntrySortFactor {
String getName(BuildContext context) { String getName(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;

View file

@ -1,5 +1,5 @@
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@immutable @immutable

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';

View file

@ -1,7 +1,8 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/geo/countries.dart'; import 'package:aves/geo/countries.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';

View file

@ -2,7 +2,8 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/origins.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';

View file

@ -1,10 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/video/channel_layouts.dart'; import 'package:aves/model/video/channel_layouts.dart';
import 'package:aves/model/video/codecs.dart'; import 'package:aves/model/video/codecs.dart';
import 'package:aves/model/video/keys.dart';
import 'package:aves/model/video/profiles/aac.dart'; import 'package:aves/model/video/profiles/aac.dart';
import 'package:aves/model/video/profiles/h264.dart'; import 'package:aves/model/video/profiles/h264.dart';
import 'package:aves/model/video/profiles/hevc.dart'; import 'package:aves/model/video/profiles/hevc.dart';
@ -17,6 +16,7 @@ import 'package:aves/utils/math_utils.dart';
import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/string_utils.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/viewer/video/fijkplayer.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fijkplayer/fijkplayer.dart'; import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -230,7 +230,7 @@ class VideoMetadataFormatter {
} }
break; break;
case Keys.codecPixelFormat: case Keys.codecPixelFormat:
if (streamType == StreamTypes.video) { if (streamType == MediaStreamTypes.video) {
// this is just a short name used by FFmpeg // this is just a short name used by FFmpeg
// user-friendly descriptions for related enums are defined in libavutil/pixfmt.h // user-friendly descriptions for related enums are defined in libavutil/pixfmt.h
save('Pixel Format', (value as String).toUpperCase()); save('Pixel Format', (value as String).toUpperCase());
@ -425,13 +425,3 @@ class VideoMetadataFormatter {
return '${(size / divider / divider).toStringAsFixed(round)} M$unit'; return '${(size / divider / divider).toStringAsFixed(round)} M$unit';
} }
} }
class StreamTypes {
static const attachment = 'attachment';
static const audio = 'audio';
static const metadata = 'metadata';
static const subtitle = 'subtitle';
static const timedText = 'timedtext';
static const unknown = 'unknown';
static const video = 'video';
}

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/model/metadata/enums/enums.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/output_buffer.dart';
import 'package:aves/services/common/service_policy.dart'; import 'package:aves/services/common/service_policy.dart';

View file

@ -1,8 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves_video/aves_video.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -12,6 +13,7 @@ abstract class MediaSessionService {
Stream<MediaCommandEvent> get mediaCommands; Stream<MediaCommandEvent> get mediaCommands;
Future<void> update({ Future<void> update({
required AvesEntry entry,
required AvesVideoController controller, required AvesVideoController controller,
required bool canSkipToNext, required bool canSkipToNext,
required bool canSkipToPrevious, required bool canSkipToPrevious,
@ -43,11 +45,11 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
@override @override
Future<void> update({ Future<void> update({
required AvesEntry entry,
required AvesVideoController controller, required AvesVideoController controller,
required bool canSkipToNext, required bool canSkipToNext,
required bool canSkipToPrevious, required bool canSkipToPrevious,
}) async { }) async {
final entry = controller.entry;
try { try {
await _platformObject.invokeMethod('update', <String, dynamic>{ await _platformObject.invokeMethod('update', <String, dynamic>{
'uri': entry.uri, 'uri': entry.uri,

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/model/metadata/enums/enums.dart'; import 'package:aves/model/metadata/enums/enums.dart';
import 'package:aves/model/metadata/enums/metadata_type.dart'; import 'package:aves/model/metadata/enums/metadata_type.dart';

View file

@ -1,4 +1,6 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/geotiff.dart'; import 'package:aves/model/geotiff.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/fields.dart'; import 'package:aves/model/metadata/fields.dart';

View file

@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/string_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/sort.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
@ -80,7 +81,7 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
entries.shuffle(); entries.shuffle();
break; break;
case WidgetDisplayedItem.mostRecent: case WidgetDisplayedItem.mostRecent:
entries.sort(AvesEntry.compareByDate); entries.sort(AvesEntrySort.compareByDate);
break; break;
} }
final entry = entries.firstOrNull; final entry = entries.firstOrNull;

View file

@ -4,6 +4,7 @@ import 'package:aves/ref/brand_colors.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/utils/dependencies.dart'; import 'package:aves/utils/dependencies.dart';
import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/about/title.dart';
import 'package:aves/widgets/about/tv_license_page.dart';
import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
@ -87,7 +88,7 @@ class _LicensesState extends State<Licenses> {
// as of Flutter v1.22.4, `cardColor` is used as a background color by `LicensePage` // as of Flutter v1.22.4, `cardColor` is used as a background color by `LicensePage`
cardColor: Theme.of(context).scaffoldBackgroundColor, cardColor: Theme.of(context).scaffoldBackgroundColor,
), ),
child: const LicensePage(), child: settings.useTvLayout ? const TvLicensePage() : const LicensePage(),
), ),
), ),
), ),

View file

@ -0,0 +1,357 @@
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/intents.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
// as of Flutter v3.7.7, `LicensePage` is not designed for Android TV
// and gets rejected from Google Play review:
// ```
// Your apps text is cut off at the edge of the screen.
// Apps should not display any text or functionality that is partially cut off by the edges of the screen.
// For example, your app (version code 94) in the "Show All Licenses" section text is cut off from the bottom of the screen.
// ```
class TvLicensePage extends StatefulWidget {
const TvLicensePage({super.key});
@override
State<TvLicensePage> createState() => _TvLicensePageState();
}
class _TvLicensePageState extends State<TvLicensePage> {
final FocusNode _railFocusNode = FocusNode();
final ScrollController _detailsScrollController = ScrollController();
final ValueNotifier<int> _railIndexNotifier = ValueNotifier(0);
final Future<_LicenseData> licenses = LicenseRegistry.licenses
.fold<_LicenseData>(
_LicenseData(),
(prev, license) => prev..addLicense(license),
)
.then((licenseData) => licenseData..sortPackages());
@override
void dispose() {
_railIndexNotifier.dispose();
_railFocusNode.dispose();
_detailsScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AvesScaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(MaterialLocalizations.of(context).licensesPageTitle),
),
body: ValueListenableBuilder<int>(
valueListenable: _railIndexNotifier,
builder: (context, selectedIndex, child) {
return FutureBuilder<_LicenseData>(
future: licenses,
builder: (context, snapshot) {
final data = snapshot.data;
if (data == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final packages = data.packages;
final rail = Focus(
focusNode: _railFocusNode,
skipTraversal: true,
canRequestFocus: false,
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size.fromWidth(300)),
child: ListView.builder(
itemBuilder: (context, index) {
final packageName = packages[index];
final bindings = data.packageLicenseBindings[packageName]!;
final isSelected = index == selectedIndex;
return Ink(
color: isSelected ? Theme.of(context).highlightColor : Theme.of(context).cardColor,
child: ListTile(
title: Text(packageName),
subtitle: Text(MaterialLocalizations.of(context).licensesPackageDetailText(bindings.length)),
selected: isSelected,
onTap: () => _railIndexNotifier.value = index,
),
);
},
itemCount: packages.length,
),
),
);
final packageName = packages[selectedIndex];
final bindings = data.packageLicenseBindings[packageName]!;
return SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 16),
rail,
Expanded(
child: FocusableActionDetector(
shortcuts: const {
SingleActivator(LogicalKeyboardKey.arrowUp): ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
SingleActivator(LogicalKeyboardKey.arrowDown): ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
},
actions: {
ScrollIntent: ScrollControllerAction(scrollController: _detailsScrollController),
},
child: KeyedSubtree(
key: Key(packageName),
child: _PackageLicensePage(
packageName: packageName,
licenseEntries: bindings.map((i) => data.licenses[i]).toList(growable: false),
scrollController: _detailsScrollController,
),
),
),
),
],
),
);
},
);
},
),
);
}
}
// adapted from Flutter `_LicenseData` in `/material/about.dart`
class _LicenseData {
final List<LicenseEntry> licenses = <LicenseEntry>[];
final Map<String, List<int>> packageLicenseBindings = <String, List<int>>{};
final List<String> packages = <String>[];
// Special treatment for the first package since it should be the package
// for delivered application.
String? firstPackage;
void addLicense(LicenseEntry entry) {
// Before the license can be added, we must first record the packages to
// which it belongs.
for (final String package in entry.packages) {
_addPackage(package);
// Bind this license to the package using the next index value. This
// creates a contract that this license must be inserted at this same
// index value.
packageLicenseBindings[package]!.add(licenses.length);
}
licenses.add(entry); // Completion of the contract above.
}
/// Add a package and initialize package license binding. This is a no-op if
/// the package has been seen before.
void _addPackage(String package) {
if (!packageLicenseBindings.containsKey(package)) {
packageLicenseBindings[package] = <int>[];
firstPackage ??= package;
packages.add(package);
}
}
/// Sort the packages using some comparison method, or by the default manner,
/// which is to put the application package first, followed by every other
/// package in case-insensitive alphabetical order.
void sortPackages([int Function(String a, String b)? compare]) {
packages.sort(compare ??
(a, b) {
// Based on how LicenseRegistry currently behaves, the first package
// returned is the end user application license. This should be
// presented first in the list. So here we make sure that first package
// remains at the front regardless of alphabetical sorting.
if (a == firstPackage) {
return -1;
}
if (b == firstPackage) {
return 1;
}
return a.toLowerCase().compareTo(b.toLowerCase());
});
}
}
// adapted from Flutter `_PackageLicensePage` in `/material/about.dart`
class _PackageLicensePage extends StatefulWidget {
const _PackageLicensePage({
required this.packageName,
required this.licenseEntries,
required this.scrollController,
});
final String packageName;
final List<LicenseEntry> licenseEntries;
final ScrollController? scrollController;
@override
_PackageLicensePageState createState() => _PackageLicensePageState();
}
class _PackageLicensePageState extends State<_PackageLicensePage> {
@override
void initState() {
super.initState();
_initLicenses();
}
final List<Widget> _licenses = <Widget>[];
bool _loaded = false;
Future<void> _initLicenses() async {
for (final LicenseEntry license in widget.licenseEntries) {
if (!mounted) {
return;
}
final List<LicenseParagraph> paragraphs = await SchedulerBinding.instance.scheduleTask<List<LicenseParagraph>>(
license.paragraphs.toList,
Priority.animation,
debugLabel: 'License',
);
if (!mounted) {
return;
}
setState(() {
_licenses.add(const Padding(
padding: EdgeInsets.all(18.0),
child: Divider(),
));
for (final LicenseParagraph paragraph in paragraphs) {
if (paragraph.indent == LicenseParagraph.centeredIndent) {
_licenses.add(Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
paragraph.text,
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
));
} else {
_licenses.add(Padding(
padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent),
child: Text(paragraph.text),
));
}
}
});
}
setState(() {
_loaded = true;
});
}
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData theme = Theme.of(context);
final String title = widget.packageName;
final String subtitle = localizations.licensesPackageDetailText(widget.licenseEntries.length);
const double pad = 24;
const EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad);
final List<Widget> listWidgets = <Widget>[
..._licenses,
if (!_loaded)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24.0),
child: Center(
child: CircularProgressIndicator(),
),
),
];
final Widget page;
if (widget.scrollController == null) {
page = Scaffold(
appBar: AppBar(
title: _PackageLicensePageTitle(
title,
subtitle,
theme.primaryTextTheme,
),
),
body: Center(
child: Material(
color: theme.cardColor,
elevation: 4.0,
child: Container(
constraints: BoxConstraints.loose(const Size.fromWidth(600.0)),
child: Localizations.override(
locale: const Locale('en', 'US'),
context: context,
child: ScrollConfiguration(
// A Scrollbar is built-in below.
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: Scrollbar(
child: ListView(padding: padding, children: listWidgets),
),
),
),
),
),
),
);
} else {
page = CustomScrollView(
controller: widget.scrollController,
slivers: <Widget>[
SliverAppBar(
automaticallyImplyLeading: false,
pinned: true,
backgroundColor: theme.cardColor,
title: _PackageLicensePageTitle(title, subtitle, theme.textTheme),
),
SliverPadding(
padding: padding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Localizations.override(
locale: const Locale('en', 'US'),
context: context,
child: listWidgets[index],
),
childCount: listWidgets.length,
),
),
),
],
);
}
return DefaultTextStyle(
style: theme.textTheme.bodySmall!,
child: page,
);
}
}
class _PackageLicensePageTitle extends StatelessWidget {
const _PackageLicensePageTitle(
this.title,
this.subtitle,
this.theme,
);
final String title;
final String subtitle;
final TextTheme theme;
@override
Widget build(BuildContext context) {
final Color? color = Theme.of(context).appBarTheme.foregroundColor;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(title, style: theme.titleLarge?.copyWith(color: color)),
Text(subtitle, style: theme.titleSmall?.copyWith(color: color)),
],
);
}
}

View file

@ -18,7 +18,7 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/accessibility_service.dart';
import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves_utils/aves_utils.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';

View file

@ -4,8 +4,10 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/entry/extensions/favourites.dart';
import 'package:aves/model/entry/extensions/metadata_edition.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/date_modifier.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -1,6 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/enums/enums.dart';

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/format.dart'; import 'package:aves/theme/format.dart';

View file

@ -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/model/source/collection_lens.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/widgets/collection/grid/headers/any.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/enums/enums.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/query_bar.dart';

View file

@ -1,6 +1,7 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/actions/share_actions.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/share_chooser.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/share_chooser.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,4 +1,5 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -4,7 +4,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves_video/aves_video.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class MuteToggler extends StatelessWidget { class MuteToggler extends StatelessWidget {

View file

@ -5,7 +5,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves_video/aves_video.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -1,5 +1,6 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/entry/extensions/metadata_edition.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/placeholder.dart'; import 'package:aves/model/filters/placeholder.dart';
import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/tag.dart';

View file

@ -3,7 +3,9 @@ import 'dart:io';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';

View file

@ -1,6 +1,9 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart';

Some files were not shown because too many files have changed in this diff Show more